diff --git a/README.md b/README.md index 6cb0994..29f33a6 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,9 @@ Command line interface for [ajv](https://github.com/epoberezkin/ajv), one of the [fastest json schema validators](https://github.com/ebdrup/json-schema-benchmark). Supports [JSON](http://json.org/), [JSON5](http://json5.org/), and [YAML](http://yaml.org/). -[![build](https://github.com/ajv-validator/ajv-cli/workflows/build/badge.svg)](https://github.com/ajv-validator/ajv-cli/actions?query=workflow%3Abuild) -[![npm](https://img.shields.io/npm/v/ajv-cli.svg)](https://www.npmjs.com/package/ajv-cli) -[![coverage](https://coveralls.io/repos/github/ajv-validator/ajv-cli/badge.svg?branch=master)](https://coveralls.io/github/ajv-validator/ajv-cli?branch=master) -[![gitter](https://img.shields.io/gitter/room/ajv-validator/ajv.svg)](https://gitter.im/ajv-validator/ajv) +[![build](https://github.com/ContinuousSecurityTooling/ajv-cli/actions/workflows/build.yml/badge.svg)](https://github.com/ContinuousSecurityTooling/ajv-cli/actions/workflows/build.yml) +[![NPM Version](https://img.shields.io/npm/v/:packageName)](https://www.npmjs.com/package/@continuoussecuritytooling/ajv-cli) +[![Coverage Status](https://coveralls.io/repos/github/ContinuousSecurityTooling/ajv-cli/badge.svg?branch=develop)](https://coveralls.io/github/ContinuousSecurityTooling/ajv-cli?branch=develop) - [ajv-cli](#ajv-cli) - [Installation](#installation) @@ -153,6 +152,7 @@ For example, you can use `-c ajv-keywords` to add all keywords from [ajv-keyword - `js` (default): JavaScript object - `json`: JSON with indentation and line-breaks + - `code-climate` emits a JSON array of CodeClimate issues to **stdout** (for easy pipe/redirect to a file). Stderr still receives the ` invalid` message. - `line`: JSON without indentation/line-breaks (for easy parsing) - `text`: human readable error messages with data paths diff --git a/src/commands/validate.ts b/src/commands/validate.ts index 71788ab..440024d 100644 --- a/src/commands/validate.ts +++ b/src/commands/validate.ts @@ -3,6 +3,21 @@ import type {ParsedArgs} from "minimist" import {compile, getFiles, openFile, logJSON} from "./util" import getAjv from "./ajv" import * as jsonPatch from "fast-json-patch" +import {createHash} from "crypto" +import type {ErrorObject} from "ajv" + +// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool +// https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types +interface CodeClimateIssue { + description: string + check_name: string + fingerprint: string + severity: "info" | "minor" | "major" | "critical" | "blocker" + location: { + path: string + lines: {begin: number} + } +} const cmd: Command = { execute, @@ -18,7 +33,7 @@ const cmd: Command = { r: {$ref: "#/$defs/stringOrArray"}, m: {$ref: "#/$defs/stringOrArray"}, c: {$ref: "#/$defs/stringOrArray"}, - errors: {enum: ["json", "line", "text", "js", "no"]}, + errors: {enum: ["json", "line", "text", "js", "no", "code-climate"]}, changes: {enum: [true, "json", "line", "js"]}, spec: {enum: ["draft7", "draft2019", "draft2020", "jtd"]}, }, @@ -28,6 +43,27 @@ const cmd: Command = { export default cmd +function formatCodeClimate(file: string, errors: ErrorObject[]): string { + const issues: CodeClimateIssue[] = errors.map((err) => { + const instancePath = err.instancePath || "/" + const message = err.message || "validation error" + const fingerprint = createHash("sha1") + .update(`${file}\0${instancePath}\0${message}`) + .digest("hex") + return { + description: `[schema] #${instancePath} ${message}`, + check_name: "json-schema", + fingerprint, + severity: "major", + location: { + path: file, + lines: {begin: 1}, + }, + } + }) + return JSON.stringify(issues, null, " ") +} + function execute(argv: ParsedArgs): boolean { const ajv = getAjv(argv) const validate = compile(ajv, argv.s) @@ -54,7 +90,11 @@ function execute(argv: ParsedArgs): boolean { } } else { console.error(file, "invalid") - console.error(logJSON(argv.errors, validate.errors, ajv)) + if (argv.errors === "code-climate") { + console.log(formatCodeClimate(file, validate.errors as ErrorObject[])) + } else { + console.error(logJSON(argv.errors, validate.errors, ajv)) + } } return validData } diff --git a/test/validate.spec.ts b/test/validate.spec.ts index f2d5eea..e2e1535 100644 --- a/test/validate.spec.ts +++ b/test/validate.spec.ts @@ -242,6 +242,68 @@ describe("validate", function () { }) }) + describe('option "errors" code-climate', () => { + it("should output valid CodeClimate JSON for invalid data", (done) => { + cli( + "-s test/schema.json -d test/invalid_data.json --errors=code-climate", + (error, stdout, stderr) => { + assert(error instanceof Error) + assert(stderr.includes("invalid")) + const issues = JSON.parse(stdout) + assert(Array.isArray(issues)) + assert(issues.length > 0) + const issue = issues[0] + assert.strictEqual(typeof issue.description, "string") + assert.strictEqual(issue.check_name, "json-schema") + assert.strictEqual(typeof issue.fingerprint, "string") + assert.strictEqual(issue.fingerprint.length, 40) + assert.strictEqual(issue.severity, "major") + assert.strictEqual(typeof issue.location.path, "string") + assert.strictEqual(typeof issue.location.lines.begin, "number") + done() + } + ) + }) + + it("should produce no issues for valid data", (done) => { + cli( + "-s test/schema.json -d test/valid_data.json --errors=code-climate", + (error, stdout, stderr) => { + assert.strictEqual(error, null) + assert(stdout.includes("valid")) + assert.strictEqual(stderr, "") + done() + } + ) + }) + + it("should include the file path in the location", (done) => { + cli( + "-s test/schema.json -d test/invalid_data.json --errors=code-climate", + (error, stdout, _stderr) => { + assert(error instanceof Error) + const issues = JSON.parse(stdout) + assert(issues[0].location.path.includes("invalid_data")) + done() + } + ) + }) + + it("should produce unique fingerprints per error", (done) => { + cli( + "-s test/schema.json -d test/invalid_data.json --errors=code-climate --all-errors", + (error, stdout, _stderr) => { + assert(error instanceof Error) + const issues = JSON.parse(stdout) + const fingerprints = issues.map((i: {fingerprint: string}) => i.fingerprint) + const unique = new Set(fingerprints) + assert.strictEqual(fingerprints.length, unique.size) + done() + } + ) + }) + }) + describe('option "changes"', () => { it("should log changes in the object after validation", (done) => { cli(