Skip to content

Commit 908c308

Browse files
PhilippHeuerP0lip
authored andcommitted
feat(formatters): add sarif formatter (#2532)
1 parent 3cbf047 commit 908c308

File tree

10 files changed

+265
-2
lines changed

10 files changed

+265
-2
lines changed

karma.conf.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ module.exports = (config: Config): void => {
2121
exclude: [
2222
'packages/cli/**',
2323
'packages/formatters/src/pretty.ts',
24+
'packages/formatters/src/github-actions.ts',
25+
'packages/formatters/src/sarif.ts',
2426
'packages/formatters/src/index.node.ts',
2527
'packages/ruleset-bundler/src/plugins/commonjs.ts',
2628
'**/*.jest.test.ts',

packages/formatters/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ console.error(output);
3333

3434
- pretty
3535
- github-actions
36+
- sarif

packages/formatters/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"chalk": "4.1.2",
4242
"cliui": "7.0.4",
4343
"lodash": "^4.17.21",
44+
"node-sarif-builder": "^2.0.3",
4445
"strip-ansi": "6.0",
4546
"text-table": "^0.2.0",
4647
"tslib": "^2.5.0"
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { DiagnosticSeverity } from '@stoplight/types';
2+
import type { IRuleResult } from '@stoplight/spectral-core';
3+
import { Ruleset } from '@stoplight/spectral-core';
4+
import { sarif } from '../sarif';
5+
6+
const cwd = process.cwd();
7+
const results: IRuleResult[] = [
8+
{
9+
code: 'operation-description',
10+
message: 'paths./pets.get.description is not truthy\nMessages can differ from the rule description',
11+
path: ['paths', '/pets', 'get', 'description'],
12+
severity: DiagnosticSeverity.Warning,
13+
source: `${cwd}/__tests__/fixtures/petstore.oas2.yaml`,
14+
range: {
15+
start: {
16+
line: 60,
17+
character: 8,
18+
},
19+
end: {
20+
line: 71,
21+
character: 60,
22+
},
23+
},
24+
},
25+
{
26+
code: 'operation-tags',
27+
message: 'paths./pets.get.tags is not truthy',
28+
path: ['paths', '/pets', 'get', 'tags'],
29+
severity: DiagnosticSeverity.Error,
30+
source: `${cwd}/__tests__/fixtures/petstore.oas2.yaml`,
31+
range: {
32+
start: {
33+
line: 60,
34+
character: 8,
35+
},
36+
end: {
37+
line: 71,
38+
character: 60,
39+
},
40+
},
41+
},
42+
];
43+
44+
describe('Sarif formatter', () => {
45+
test('should be formatted correctly', async () => {
46+
const sarifToolVersion = '6.11';
47+
const ruleset = new Ruleset({
48+
rules: {
49+
'operation-description': {
50+
description: 'paths./pets.get.description is not truthy',
51+
message: 'paths./pets.get.description is not truthy\nMessages can differ from the rule description',
52+
severity: DiagnosticSeverity.Error,
53+
given: '$.paths[*][*]',
54+
then: {
55+
field: 'description',
56+
function: function truthy() {
57+
return false;
58+
},
59+
},
60+
},
61+
'operation-tags': {
62+
description: 'paths./pets.get.tags is not truthy',
63+
message: 'paths./pets.get.tags is not truthy\nMessages can differ from the rule description',
64+
severity: DiagnosticSeverity.Error,
65+
given: '$.paths[*][*]',
66+
then: {
67+
field: 'description',
68+
function: function truthy() {
69+
return false;
70+
},
71+
},
72+
},
73+
},
74+
});
75+
76+
const output = sarif(
77+
results,
78+
{ failSeverity: DiagnosticSeverity.Error },
79+
{ ruleset, spectralVersion: sarifToolVersion },
80+
);
81+
82+
const outputObject = JSON.parse(output);
83+
expect(outputObject).toStrictEqual({
84+
$schema: 'http://json.schemastore.org/sarif-2.1.0-rtm.6.json',
85+
version: '2.1.0',
86+
runs: [
87+
{
88+
tool: {
89+
driver: {
90+
name: 'spectral',
91+
rules: [
92+
{
93+
id: 'operation-description',
94+
shortDescription: {
95+
text: 'paths./pets.get.description is not truthy',
96+
},
97+
},
98+
{
99+
id: 'operation-tags',
100+
shortDescription: {
101+
text: 'paths./pets.get.tags is not truthy',
102+
},
103+
},
104+
],
105+
version: sarifToolVersion,
106+
informationUri: 'https://github.com/stoplightio/spectral',
107+
},
108+
},
109+
results: [
110+
{
111+
level: 'warning',
112+
message: {
113+
text: 'paths./pets.get.description is not truthy\nMessages can differ from the rule description',
114+
},
115+
ruleId: 'operation-description',
116+
locations: [
117+
{
118+
physicalLocation: {
119+
artifactLocation: {
120+
uri: '__tests__/fixtures/petstore.oas2.yaml',
121+
index: 0,
122+
},
123+
region: {
124+
startLine: 61,
125+
startColumn: 9,
126+
endLine: 72,
127+
endColumn: 61,
128+
},
129+
},
130+
},
131+
],
132+
ruleIndex: 0,
133+
},
134+
{
135+
level: 'error',
136+
message: {
137+
text: 'paths./pets.get.tags is not truthy',
138+
},
139+
ruleId: 'operation-tags',
140+
locations: [
141+
{
142+
physicalLocation: {
143+
artifactLocation: {
144+
uri: '__tests__/fixtures/petstore.oas2.yaml',
145+
index: 0,
146+
},
147+
region: {
148+
startLine: 61,
149+
startColumn: 9,
150+
endLine: 72,
151+
endColumn: 61,
152+
},
153+
},
154+
},
155+
],
156+
ruleIndex: 1,
157+
},
158+
],
159+
artifacts: [
160+
{
161+
sourceLanguage: 'YAML',
162+
location: {
163+
uri: '__tests__/fixtures/petstore.oas2.yaml',
164+
},
165+
},
166+
],
167+
},
168+
],
169+
});
170+
});
171+
});

packages/formatters/src/index.node.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { html, json, junit, text, stylish, teamcity } from './index';
22
export type { Formatter, FormatterOptions } from './index';
33
export { pretty } from './pretty';
44
export { githubActions } from './github-actions';
5+
export { sarif } from './sarif';

packages/formatters/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@ export const pretty: Formatter = () => {
1414
export const githubActions: Formatter = () => {
1515
throw Error('github-actions formatter is available only in Node.js');
1616
};
17+
18+
export const sarif: Formatter = () => {
19+
throw Error('sarif formatter is available only in Node.js');
20+
};

packages/formatters/src/sarif.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { DiagnosticSeverity, Dictionary } from '@stoplight/types';
2+
import { relative } from '@stoplight/path';
3+
import { SarifBuilder, SarifRunBuilder, SarifResultBuilder, SarifRuleBuilder } from 'node-sarif-builder';
4+
import type { Result } from 'sarif';
5+
import type { Formatter } from './types';
6+
7+
const OUTPUT_TYPES: Dictionary<Result.level, DiagnosticSeverity> = {
8+
[DiagnosticSeverity.Error]: 'error',
9+
[DiagnosticSeverity.Warning]: 'warning',
10+
[DiagnosticSeverity.Information]: 'note',
11+
[DiagnosticSeverity.Hint]: 'note',
12+
};
13+
14+
export const sarif: Formatter = (results, _, ctx) => {
15+
if (ctx === void 0) {
16+
throw Error('sarif formatter requires ctx');
17+
}
18+
19+
const sarifBuilder = new SarifBuilder({
20+
$schema: 'http://json.schemastore.org/sarif-2.1.0-rtm.6.json',
21+
version: '2.1.0',
22+
runs: [],
23+
});
24+
25+
const sarifRunBuilder = new SarifRunBuilder().initSimple({
26+
toolDriverName: 'spectral',
27+
toolDriverVersion: ctx.spectralVersion,
28+
url: 'https://github.com/stoplightio/spectral',
29+
});
30+
31+
// add rules
32+
for (const rule of Object.values(ctx.ruleset.rules)) {
33+
const sarifRuleBuilder = new SarifRuleBuilder().initSimple({
34+
ruleId: rule.name,
35+
shortDescriptionText: rule.description ?? 'No description.',
36+
helpUri: rule.documentationUrl !== null ? rule.documentationUrl : undefined,
37+
});
38+
sarifRunBuilder.addRule(sarifRuleBuilder);
39+
}
40+
41+
// add results
42+
for (const result of results) {
43+
const sarifResultBuilder = new SarifResultBuilder();
44+
const severity: DiagnosticSeverity = result.severity || DiagnosticSeverity.Error;
45+
sarifResultBuilder.initSimple({
46+
level: OUTPUT_TYPES[severity] || 'error',
47+
messageText: result.message,
48+
ruleId: result.code.toString(),
49+
fileUri: relative(process.cwd(), result.source ?? '').replace(/\\/g, '/'),
50+
startLine: result.range.start.line + 1,
51+
startColumn: result.range.start.character + 1,
52+
endLine: result.range.end.line + 1,
53+
endColumn: result.range.end.character + 1,
54+
});
55+
sarifRunBuilder.addResult(sarifResultBuilder);
56+
}
57+
58+
sarifBuilder.addRun(sarifRunBuilder);
59+
return sarifBuilder.buildSarifJsonString({ indent: true });
60+
};

packages/formatters/src/types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import { ISpectralDiagnostic } from '@stoplight/spectral-core';
1+
import { ISpectralDiagnostic, Ruleset } from '@stoplight/spectral-core';
22
import type { DiagnosticSeverity } from '@stoplight/types';
33

44
export type FormatterOptions = {
55
failSeverity: DiagnosticSeverity;
66
};
77

8-
export type Formatter = (results: ISpectralDiagnostic[], options: FormatterOptions) => string;
8+
export type FormatterContext = {
9+
ruleset: Ruleset;
10+
spectralVersion: string;
11+
};
12+
13+
export type Formatter = (results: ISpectralDiagnostic[], options: FormatterOptions, ctx?: FormatterContext) => string;

yarn.lock

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2730,6 +2730,7 @@ __metadata:
27302730
eol: 0.9.1
27312731
lodash: ^4.17.21
27322732
node-html-parser: ^4.1.5
2733+
node-sarif-builder: ^2.0.3
27332734
strip-ansi: 6.0
27342735
text-table: ^0.2.0
27352736
tslib: ^2.5.0
@@ -3342,6 +3343,13 @@ __metadata:
33423343
languageName: node
33433344
linkType: hard
33443345

3346+
"@types/sarif@npm:^2.1.4":
3347+
version: 2.1.4
3348+
resolution: "@types/sarif@npm:2.1.4"
3349+
checksum: 1ff924e9ffe468f93c8751d6e8192ca126380a328ba7d8f7abb6d3e7d66080f9d3c93c4db94ddca569b65a2f6d3b82dfe9b79f23500ebb69e0f6d2d12a1dc5c4
3350+
languageName: node
3351+
linkType: hard
3352+
33453353
"@types/stack-utils@npm:^2.0.0":
33463354
version: 2.0.0
33473355
resolution: "@types/stack-utils@npm:2.0.0"
@@ -9805,6 +9813,16 @@ __metadata:
98059813
languageName: node
98069814
linkType: hard
98079815

9816+
"node-sarif-builder@npm:^2.0.3":
9817+
version: 2.0.3
9818+
resolution: "node-sarif-builder@npm:2.0.3"
9819+
dependencies:
9820+
"@types/sarif": ^2.1.4
9821+
fs-extra: ^10.0.0
9822+
checksum: 397dd9bfb0780c6753fb47d1fd0465f3c8a935082cb1bbd7ad6232d18b6343d9d499c6bc572ad0415db282efd6058fe8b7a6657020434adef4fbf93a8b95306e
9823+
languageName: node
9824+
linkType: hard
9825+
98089826
"nopt@npm:^5.0.0":
98099827
version: 5.0.0
98109828
resolution: "nopt@npm:5.0.0"

0 commit comments

Comments
 (0)