Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding SARIF support #481

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Options:
-s, --skip [ruleName] provide multiple rules to skip
-j, --json-schema treat $ref like JSON Schema and convert to OpenAPI Schema Objects
-v, --verbose set verbosity (use multiple times to increase level)
-f, --format [format] result format (sarif or default)
-h, --help output usage information
```

Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Options:
-s, --skip [ruleName] provide multiple rules to skip
-j, --json-schema treat $ref like JSON Schema and convert to OpenAPI Schema Objects
-v, --verbose increase verbosity
-f, --format [format] result format
-h, --help output usage information
```

Expand Down
1 change: 1 addition & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Config {
lint: {
rules: this.notEmptyArray(args.rules),
skip: this.notEmptyArray(args.skip),
format: args.format,
},
resolve: {
output: args.output,
Expand Down
52 changes: 50 additions & 2 deletions lint.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

'use strict'

const fs = require('fs');
const url = require('url');
const path = require('path');
const config = require('./lib/config.js');
const loader = require('./lib/loader.js');
const linter = require('oas-linter');
Expand Down Expand Up @@ -49,7 +52,7 @@ const truncateLongMessages = message => {
return lines.join('\n');
}

const formatLintResults = lintResults => {
const formatLintResultsAsDefault = lintResults => {
let output = '';
lintResults.forEach(result => {
const { rule, error, pointer } = result;
Expand All @@ -65,12 +68,57 @@ More information: ${rule.url}#${rule.name}
return output;
}


const formatLintResults = (lintResults, format, specFile) => {
if (format === 'sarif') {
return formatLintResultsAsSarif(lintResults, specFile);
}
else {
return formatLintResultsAsDefault(lintResults);
}
}

const formatLintResultsAsSarif = (lintResults, specFile) => {
const specFileDir = path.dirname(specFile)
const specFileName = path.basename(specFile)
const specFileDirFileUrl = url.pathToFileURL(specFileDir)
const specFileDirFileUrlString = specFileDirFileUrl.toString() + '/'
const templateFile = path.resolve(__dirname, 'templates/sarif_template.json');
const template = fs.readFileSync(templateFile);
let sarif = JSON.parse(template);
sarif['runs'][0]['originalUriBaseIds']['ROOTPATH']['uri'] = specFileDirFileUrlString;
sarif['runs'][0]['tool']['driver']['rules'] = [];
sarif['runs'][0]['results'] = [];

lintResults.forEach(result => {
const { rule, error, pointer } = result;
const ruleExists = sarif['runs'][0]['tool']['driver']['rules'].some(it => it.id === rule.name);
if (!ruleExists){
var newRule = { 'id' : rule.name};
newRule['shortDescription'] = { 'text' : rule.description };
newRule['helpUri'] = rule.url + "#" + rule.name;
sarif['runs'][0]['tool']['driver']['rules'].push(newRule);
}
const ruleIndex = sarif['runs'][0]['tool']['driver']['rules'].findIndex(it => it.id === rule.name);
var result = {
'ruleId' : rule.name,
'ruleIndex' : ruleIndex,
'message' : { 'text' : result.dataPath + ': ' + result.message },
'locations' : [{ 'physicalLocation' : { 'artifactLocation' : { 'uri' : specFileName, 'uriBaseId' : 'ROOTPATH' } } }],
};
sarif['runs'][0]['results'].push(result);
});

return JSON.stringify(sarif);
}

const command = async (specFile, cmd) => {
config.init(cmd);
const jsonSchema = config.get('jsonSchema');
const verbose = config.get('quiet') ? 0 : config.get('verbose', 1);
const rulesets = config.get('lint:rules', []);
const skip = config.get('lint:skip', []);
const format = config.get('lint:format');

rules.init({
skip
Expand Down Expand Up @@ -101,7 +149,7 @@ const command = async (specFile, cmd) => {

if (warnings.length) {
console.error(colors.red + 'Specification contains lint errors: ' + warnings.length + colors.reset);
console.warn(formatLintResults(warnings))
console.warn(formatLintResults(warnings, format, specFile))
return reject();
}

Expand Down
1 change: 1 addition & 0 deletions speccy.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ program
.option('-s, --skip [ruleName]', 'provide multiple rules to skip', collect, [])
.option('-j, --json-schema', 'treat $ref like JSON Schema and convert to OpenAPI Schema Objects (default: false)')
.option('-v, --verbose', 'increase verbosity', increaseVerbosity, 1)
.option('-f, --format [format]', 'Result format, currently support sarif format and default format.')
.action((specFile, cmd) => {
lint.command(specFile, cmd)
.then(() => { process.exit(0) })
Expand Down
49 changes: 49 additions & 0 deletions templates/sarif_template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"$schema":"https://www.schemastore.org/schemas/json/sarif-2.1.0-rtm.5.json",
"version":"2.1.0",
"runs":[
{
"tool":{
"driver":{
"name":"Speccy",
"informationUri":"http://speccy.io/",
"version":"0.11.0",
"rules":[
{
"id":"info-contact",
"shortDescription":{
"text":"info object should contain contact object"
},
"helpUri": "https://speccy.io/rules/1-rulesets"
}
]
}
},
"results":[
{
"ruleId":"info-contact",
"ruleIndex":0,
"message":{
"text":"#/info: expected Object { version: '1.0.0', title: 'Swagger 2.0 Without Scheme' } to have property contact"
},
"locations":[
{
"physicalLocation":{
"artifactLocation":{
"uri":"no-contact.yaml",
"uriBaseId":"ROOTPATH"
}
}
}
]
}
],
"columnKind":"utf16CodeUnits",
"originalUriBaseIds":{
"ROOTPATH":{
"uri":"file:///home/user/testopenapidir/"
}
}
}
]
}
27 changes: 27 additions & 0 deletions test/integration/lint.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ const commandConfig = {
skip: []
};

const commandConfigSarif = {
quiet: false,
verbose: false,
rules: [],
skip: [],
format: 'sarif'
};

beforeEach(() => {
jest.restoreAllMocks();
});
Expand Down Expand Up @@ -77,6 +85,25 @@ describe('Lint command', () => {
});
});

describe('properly handles linter warnings with format SARIF', () => {
test('displays a linting error on missing contact field with format SARIF', () => {
expect.assertions(7);
const logSpy = jest.spyOn(console, 'log');
const warnSpy = jest.spyOn(console, 'warn');
const errorSpy = jest.spyOn(console, 'error');

return lint.command('./test/fixtures/integration/missing-contact.yaml', commandConfigSarif).catch(() => {
expect(logSpy).toBeCalledTimes(0);
expect(warnSpy).toBeCalledTimes(1);
expect(errorSpy).toBeCalledTimes(1);
expect(errorSpy.mock.calls[0][0]).toEqual('\x1b[31mSpecification contains lint errors: 1\x1b[0m');
expect(warnSpy.mock.calls[0][0]).toContain('"tool":{"driver":{"name":"Speccy","informationUri":"http://speccy.io/"');
expect(warnSpy.mock.calls[0][0]).toContain('"results":[{"ruleId":"info-contact"');
expect(warnSpy.mock.calls[0][0]).toContain('"helpUri":"https://speccy.io/rules/1-rulesets#info-contact"');
});
});
});

describe('properly support stdin and pipes', () => {
let stdin = null;

Expand Down