From 908c3081df514dbb11c5f6b379f5ad49442c1e0c Mon Sep 17 00:00:00 2001 From: Philipp Heuer Date: Tue, 5 Sep 2023 19:02:00 +0200 Subject: [PATCH] feat(formatters): add sarif formatter (#2532) --- karma.conf.ts | 2 + packages/formatters/README.md | 1 + packages/formatters/package.json | 1 + ...ns.test.ts => github-actions.jest.test.ts} | 0 .../src/__tests__/sarif.jest.test.ts | 171 ++++++++++++++++++ packages/formatters/src/index.node.ts | 1 + packages/formatters/src/index.ts | 4 + packages/formatters/src/sarif.ts | 60 ++++++ packages/formatters/src/types.ts | 9 +- yarn.lock | 18 ++ 10 files changed, 265 insertions(+), 2 deletions(-) rename packages/formatters/src/__tests__/{github-actions.test.ts => github-actions.jest.test.ts} (100%) create mode 100644 packages/formatters/src/__tests__/sarif.jest.test.ts create mode 100644 packages/formatters/src/sarif.ts diff --git a/karma.conf.ts b/karma.conf.ts index 6bd9cd705..1f04c4589 100644 --- a/karma.conf.ts +++ b/karma.conf.ts @@ -21,6 +21,8 @@ module.exports = (config: Config): void => { exclude: [ 'packages/cli/**', 'packages/formatters/src/pretty.ts', + 'packages/formatters/src/github-actions.ts', + 'packages/formatters/src/sarif.ts', 'packages/formatters/src/index.node.ts', 'packages/ruleset-bundler/src/plugins/commonjs.ts', '**/*.jest.test.ts', diff --git a/packages/formatters/README.md b/packages/formatters/README.md index ebade72f6..809d02e99 100644 --- a/packages/formatters/README.md +++ b/packages/formatters/README.md @@ -33,3 +33,4 @@ console.error(output); - pretty - github-actions +- sarif diff --git a/packages/formatters/package.json b/packages/formatters/package.json index 96acb6e0f..b5c84fafd 100644 --- a/packages/formatters/package.json +++ b/packages/formatters/package.json @@ -41,6 +41,7 @@ "chalk": "4.1.2", "cliui": "7.0.4", "lodash": "^4.17.21", + "node-sarif-builder": "^2.0.3", "strip-ansi": "6.0", "text-table": "^0.2.0", "tslib": "^2.5.0" diff --git a/packages/formatters/src/__tests__/github-actions.test.ts b/packages/formatters/src/__tests__/github-actions.jest.test.ts similarity index 100% rename from packages/formatters/src/__tests__/github-actions.test.ts rename to packages/formatters/src/__tests__/github-actions.jest.test.ts diff --git a/packages/formatters/src/__tests__/sarif.jest.test.ts b/packages/formatters/src/__tests__/sarif.jest.test.ts new file mode 100644 index 000000000..2e3ef2e07 --- /dev/null +++ b/packages/formatters/src/__tests__/sarif.jest.test.ts @@ -0,0 +1,171 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import type { IRuleResult } from '@stoplight/spectral-core'; +import { Ruleset } from '@stoplight/spectral-core'; +import { sarif } from '../sarif'; + +const cwd = process.cwd(); +const results: IRuleResult[] = [ + { + code: 'operation-description', + message: 'paths./pets.get.description is not truthy\nMessages can differ from the rule description', + path: ['paths', '/pets', 'get', 'description'], + severity: DiagnosticSeverity.Warning, + source: `${cwd}/__tests__/fixtures/petstore.oas2.yaml`, + range: { + start: { + line: 60, + character: 8, + }, + end: { + line: 71, + character: 60, + }, + }, + }, + { + code: 'operation-tags', + message: 'paths./pets.get.tags is not truthy', + path: ['paths', '/pets', 'get', 'tags'], + severity: DiagnosticSeverity.Error, + source: `${cwd}/__tests__/fixtures/petstore.oas2.yaml`, + range: { + start: { + line: 60, + character: 8, + }, + end: { + line: 71, + character: 60, + }, + }, + }, +]; + +describe('Sarif formatter', () => { + test('should be formatted correctly', async () => { + const sarifToolVersion = '6.11'; + const ruleset = new Ruleset({ + rules: { + 'operation-description': { + description: 'paths./pets.get.description is not truthy', + message: 'paths./pets.get.description is not truthy\nMessages can differ from the rule description', + severity: DiagnosticSeverity.Error, + given: '$.paths[*][*]', + then: { + field: 'description', + function: function truthy() { + return false; + }, + }, + }, + 'operation-tags': { + description: 'paths./pets.get.tags is not truthy', + message: 'paths./pets.get.tags is not truthy\nMessages can differ from the rule description', + severity: DiagnosticSeverity.Error, + given: '$.paths[*][*]', + then: { + field: 'description', + function: function truthy() { + return false; + }, + }, + }, + }, + }); + + const output = sarif( + results, + { failSeverity: DiagnosticSeverity.Error }, + { ruleset, spectralVersion: sarifToolVersion }, + ); + + const outputObject = JSON.parse(output); + expect(outputObject).toStrictEqual({ + $schema: 'http://json.schemastore.org/sarif-2.1.0-rtm.6.json', + version: '2.1.0', + runs: [ + { + tool: { + driver: { + name: 'spectral', + rules: [ + { + id: 'operation-description', + shortDescription: { + text: 'paths./pets.get.description is not truthy', + }, + }, + { + id: 'operation-tags', + shortDescription: { + text: 'paths./pets.get.tags is not truthy', + }, + }, + ], + version: sarifToolVersion, + informationUri: 'https://github.com/stoplightio/spectral', + }, + }, + results: [ + { + level: 'warning', + message: { + text: 'paths./pets.get.description is not truthy\nMessages can differ from the rule description', + }, + ruleId: 'operation-description', + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: '__tests__/fixtures/petstore.oas2.yaml', + index: 0, + }, + region: { + startLine: 61, + startColumn: 9, + endLine: 72, + endColumn: 61, + }, + }, + }, + ], + ruleIndex: 0, + }, + { + level: 'error', + message: { + text: 'paths./pets.get.tags is not truthy', + }, + ruleId: 'operation-tags', + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: '__tests__/fixtures/petstore.oas2.yaml', + index: 0, + }, + region: { + startLine: 61, + startColumn: 9, + endLine: 72, + endColumn: 61, + }, + }, + }, + ], + ruleIndex: 1, + }, + ], + artifacts: [ + { + sourceLanguage: 'YAML', + location: { + uri: '__tests__/fixtures/petstore.oas2.yaml', + }, + }, + ], + }, + ], + }); + }); +}); diff --git a/packages/formatters/src/index.node.ts b/packages/formatters/src/index.node.ts index d9e9b3ce2..e9d7a5b39 100644 --- a/packages/formatters/src/index.node.ts +++ b/packages/formatters/src/index.node.ts @@ -2,3 +2,4 @@ export { html, json, junit, text, stylish, teamcity } from './index'; export type { Formatter, FormatterOptions } from './index'; export { pretty } from './pretty'; export { githubActions } from './github-actions'; +export { sarif } from './sarif'; diff --git a/packages/formatters/src/index.ts b/packages/formatters/src/index.ts index 8caf77087..23f612c6d 100644 --- a/packages/formatters/src/index.ts +++ b/packages/formatters/src/index.ts @@ -14,3 +14,7 @@ export const pretty: Formatter = () => { export const githubActions: Formatter = () => { throw Error('github-actions formatter is available only in Node.js'); }; + +export const sarif: Formatter = () => { + throw Error('sarif formatter is available only in Node.js'); +}; diff --git a/packages/formatters/src/sarif.ts b/packages/formatters/src/sarif.ts new file mode 100644 index 000000000..76c9f74eb --- /dev/null +++ b/packages/formatters/src/sarif.ts @@ -0,0 +1,60 @@ +import { DiagnosticSeverity, Dictionary } from '@stoplight/types'; +import { relative } from '@stoplight/path'; +import { SarifBuilder, SarifRunBuilder, SarifResultBuilder, SarifRuleBuilder } from 'node-sarif-builder'; +import type { Result } from 'sarif'; +import type { Formatter } from './types'; + +const OUTPUT_TYPES: Dictionary = { + [DiagnosticSeverity.Error]: 'error', + [DiagnosticSeverity.Warning]: 'warning', + [DiagnosticSeverity.Information]: 'note', + [DiagnosticSeverity.Hint]: 'note', +}; + +export const sarif: Formatter = (results, _, ctx) => { + if (ctx === void 0) { + throw Error('sarif formatter requires ctx'); + } + + const sarifBuilder = new SarifBuilder({ + $schema: 'http://json.schemastore.org/sarif-2.1.0-rtm.6.json', + version: '2.1.0', + runs: [], + }); + + const sarifRunBuilder = new SarifRunBuilder().initSimple({ + toolDriverName: 'spectral', + toolDriverVersion: ctx.spectralVersion, + url: 'https://github.com/stoplightio/spectral', + }); + + // add rules + for (const rule of Object.values(ctx.ruleset.rules)) { + const sarifRuleBuilder = new SarifRuleBuilder().initSimple({ + ruleId: rule.name, + shortDescriptionText: rule.description ?? 'No description.', + helpUri: rule.documentationUrl !== null ? rule.documentationUrl : undefined, + }); + sarifRunBuilder.addRule(sarifRuleBuilder); + } + + // add results + for (const result of results) { + const sarifResultBuilder = new SarifResultBuilder(); + const severity: DiagnosticSeverity = result.severity || DiagnosticSeverity.Error; + sarifResultBuilder.initSimple({ + level: OUTPUT_TYPES[severity] || 'error', + messageText: result.message, + ruleId: result.code.toString(), + fileUri: relative(process.cwd(), result.source ?? '').replace(/\\/g, '/'), + startLine: result.range.start.line + 1, + startColumn: result.range.start.character + 1, + endLine: result.range.end.line + 1, + endColumn: result.range.end.character + 1, + }); + sarifRunBuilder.addResult(sarifResultBuilder); + } + + sarifBuilder.addRun(sarifRunBuilder); + return sarifBuilder.buildSarifJsonString({ indent: true }); +}; diff --git a/packages/formatters/src/types.ts b/packages/formatters/src/types.ts index 80607838e..8624af331 100644 --- a/packages/formatters/src/types.ts +++ b/packages/formatters/src/types.ts @@ -1,8 +1,13 @@ -import { ISpectralDiagnostic } from '@stoplight/spectral-core'; +import { ISpectralDiagnostic, Ruleset } from '@stoplight/spectral-core'; import type { DiagnosticSeverity } from '@stoplight/types'; export type FormatterOptions = { failSeverity: DiagnosticSeverity; }; -export type Formatter = (results: ISpectralDiagnostic[], options: FormatterOptions) => string; +export type FormatterContext = { + ruleset: Ruleset; + spectralVersion: string; +}; + +export type Formatter = (results: ISpectralDiagnostic[], options: FormatterOptions, ctx?: FormatterContext) => string; diff --git a/yarn.lock b/yarn.lock index 80fa76eda..626cc7257 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2730,6 +2730,7 @@ __metadata: eol: 0.9.1 lodash: ^4.17.21 node-html-parser: ^4.1.5 + node-sarif-builder: ^2.0.3 strip-ansi: 6.0 text-table: ^0.2.0 tslib: ^2.5.0 @@ -3342,6 +3343,13 @@ __metadata: languageName: node linkType: hard +"@types/sarif@npm:^2.1.4": + version: 2.1.4 + resolution: "@types/sarif@npm:2.1.4" + checksum: 1ff924e9ffe468f93c8751d6e8192ca126380a328ba7d8f7abb6d3e7d66080f9d3c93c4db94ddca569b65a2f6d3b82dfe9b79f23500ebb69e0f6d2d12a1dc5c4 + languageName: node + linkType: hard + "@types/stack-utils@npm:^2.0.0": version: 2.0.0 resolution: "@types/stack-utils@npm:2.0.0" @@ -9805,6 +9813,16 @@ __metadata: languageName: node linkType: hard +"node-sarif-builder@npm:^2.0.3": + version: 2.0.3 + resolution: "node-sarif-builder@npm:2.0.3" + dependencies: + "@types/sarif": ^2.1.4 + fs-extra: ^10.0.0 + checksum: 397dd9bfb0780c6753fb47d1fd0465f3c8a935082cb1bbd7ad6232d18b6343d9d499c6bc572ad0415db282efd6058fe8b7a6657020434adef4fbf93a8b95306e + languageName: node + linkType: hard + "nopt@npm:^5.0.0": version: 5.0.0 resolution: "nopt@npm:5.0.0"