Skip to content

Commit

Permalink
feat(formatters): add sarif formatter (#2532)
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippHeuer authored and P0lip committed Sep 14, 2023
1 parent 3cbf047 commit 908c308
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 2 deletions.
2 changes: 2 additions & 0 deletions karma.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions packages/formatters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ console.error(output);

- pretty
- github-actions
- sarif
1 change: 1 addition & 0 deletions packages/formatters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
171 changes: 171 additions & 0 deletions packages/formatters/src/__tests__/sarif.jest.test.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
],
},
],
});
});
});
1 change: 1 addition & 0 deletions packages/formatters/src/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
4 changes: 4 additions & 0 deletions packages/formatters/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
};
60 changes: 60 additions & 0 deletions packages/formatters/src/sarif.ts
Original file line number Diff line number Diff line change
@@ -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<Result.level, DiagnosticSeverity> = {
[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 });
};
9 changes: 7 additions & 2 deletions packages/formatters/src/types.ts
Original file line number Diff line number Diff line change
@@ -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;
18 changes: 18 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 908c308

Please sign in to comment.