diff --git a/README.md b/README.md index 70ee4a6f..b6a69e15 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,35 @@ Licensed under the MIT License. # axe-sarif-converter -**An [axe-core](https://github.com/dequelabs/axe-core) reporter that outputs axe scan results in [SARIF format](http://sarifweb.azurewebsites.net/).** +Convert [axe-core](https://github.com/dequelabs/axe-core) accessibility scan results to the [SARIF format](http://sarifweb.azurewebsites.net/). + +Use this with the [Sarif Viewer Build Tab Azure DevOps Extension](https://marketplace.visualstudio.com/items?itemName=sariftools.sarif-viewer-build-tab) to visualize accessibility scan results in the build results of an [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/) build. + +## Usage + +Before using axe-sarif-converter, you will need to run an [axe](https://github.com/dequelabs/axe-core) accessibility scan to produce some axe results to convert. Typically, you would do this by using an axe integration library for your favorite browser automation tool ([axe-puppeteer](https://github.com/dequelabs/axe-puppeteer), [axe-webdriverjs](https://github.com/dequelabs/axe-webdriverjs), [cypress-axe](https://github.com/avanslaars/cypress-axe)). + +axe-sarif-converter exports a single function, named `convertAxeToSarif`. Use it like this: + +```ts +import { convertAxeToSarif, SarifLog } from 'axe-sarif-converter'; + +test('my accessibility test', async () => { + // This example uses axe-puppeteer, but you can use any axe-based library + // that outputs axe scan results in the default axe output format + const testPage: Puppeteer.Page = /* ... set up your test page ... */; + const axeResults: Axe.AxeResults = await new AxePuppeteer(testPage).analyze(); + + // Perform the conversion + const sarifResults: SarifLog = convertAxeToSarif(axeResults); + + // Output a SARIF file, perhaps for use with a Sarif Viewer tool + await fs.promises.writeFile( + './test-results/my-accessibility-test.sarif', + JSON.stringify(sarifResults), + { encoding: 'utf8' }); +} +``` ## Contributing diff --git a/package.json b/package.json index 3493a8e8..2f753da4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "axe-sarif-converter", - "version": "0.0.1", - "description": "An axe-core reporter that outputs axe scan results in SARIF format (http://sarifweb.azurewebsites.net/)", + "version": "1.0.0", + "description": "Convert axe-core accessibility scan results to the SARIF format", "main": "dist/index.js", "types": "dist/index.d.js", "files": [ diff --git a/src/decorated-axe-results.ts b/src/decorated-axe-results.ts new file mode 100644 index 00000000..93ed2acb --- /dev/null +++ b/src/decorated-axe-results.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as Axe from 'axe-core'; +import { WCAG } from './wcag'; + +export interface AxeResultDecorations { + WCAG?: WCAG[]; +} + +export type DecoratedAxeResult = Axe.Result & AxeResultDecorations; + +export interface DecoratedAxeResults { + passes: DecoratedAxeResult[]; + violations: DecoratedAxeResult[]; + inapplicable: DecoratedAxeResult[]; + incomplete: DecoratedAxeResult[]; + timestamp: string; + targetPageUrl: string; + targetPageTitle: string; +} diff --git a/src/index.test.ts b/src/index.test.ts index 5c19dc3e..be9135a2 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -9,9 +9,9 @@ import { TestRunner, } from 'axe-core'; import * as fs from 'fs'; -import { axeToSarif } from './index'; -import { SarifLog } from './sarif/sarifLog'; -import { Run } from './sarif/sarifv2'; +import { convertAxeToSarif } from './index'; +import { Run } from './sarif/sarif-2.0.0'; +import { SarifLog } from './sarif/sarif-log'; describe('axe-sarf-converter integration test', () => { it('axe-sarif-converter returns a valid sarif with blank results array in run array', () => { @@ -64,8 +64,8 @@ describe('axe-sarf-converter integration test', () => { ] as Run[], }; - expect(axeToSarif(axeResultStub)).toBeDefined(); - expect(axeToSarif(axeResultStub)).toEqual(expected); + expect(convertAxeToSarif(axeResultStub)).toBeDefined(); + expect(convertAxeToSarif(axeResultStub)).toEqual(expected); }); }); @@ -76,6 +76,6 @@ describe('axeToSarifConverter use generated AxeResults object', () => { 'utf8', ); const axeResultStub: AxeResults = JSON.parse(axeJSON) as AxeResults; - expect(axeToSarif(axeResultStub)).toMatchSnapshot(); + expect(convertAxeToSarif(axeResultStub)).toMatchSnapshot(); }); }); diff --git a/src/index.ts b/src/index.ts index e3b32bf7..46d711c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,27 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as Axe from 'axe-core'; -import { ConverterOptions } from './converter-options'; import { ResultDecorator } from './result-decorator'; import { SarifConverter } from './sarif-converter'; -import { SarifLog } from './sarif/sarifLog'; +import { SarifLog } from './sarif/sarif-log'; import { rulesWCAGConfiguration } from './wcag-mappings'; -export { ConverterOptions } from './converter-options'; - -export function axeToSarif( - axeResults: Axe.AxeResults, - options?: ConverterOptions, -): SarifLog { - options = options ? options : {}; - +export function convertAxeToSarif(axeResults: Axe.AxeResults): SarifLog { const resultDecorator = new ResultDecorator(rulesWCAGConfiguration); + const decoratedAxeResults = resultDecorator.decorateResults(axeResults); const sarifConverter = new SarifConverter(); - - // AxeResults -> ScannerResults - const scannerResults = resultDecorator.decorateResults(axeResults); - - // ScannerResults -> ISarifLog - return sarifConverter.convert(scannerResults, options); + return sarifConverter.convert(decoratedAxeResults, {}); } diff --git a/src/result-decorator.test.ts b/src/result-decorator.test.ts index 1c9770f7..b39f1c2e 100644 --- a/src/result-decorator.test.ts +++ b/src/result-decorator.test.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { AxeResults, Result } from 'axe-core'; +import { DecoratedAxeResults } from './decorated-axe-results'; import { DictionaryStringTo } from './dictionary-types'; import { ResultDecorator } from './result-decorator'; -import { ScannerResults } from './ruleresults'; import { WCAG } from './wcag'; describe('Result Decorator', () => { @@ -25,11 +25,11 @@ describe('Result Decorator', () => { const wcagInfo: DictionaryStringTo = {}; const resultDecorator = new ResultDecorator(wcagInfo); - const decoratedResult: ScannerResults = resultDecorator.decorateResults( + const decoratedResults: DecoratedAxeResults = resultDecorator.decorateResults( resultStub, ); - expect(decoratedResult.violations).toEqual([]); - expect(resultDecorator.decorateResults(resultStub)).toMatchSnapshot(); + expect(decoratedResults.violations).toEqual([]); + expect(decoratedResults).toMatchSnapshot(); }); it('result decorator contains WCAG information that is provided as dependency', () => { @@ -73,10 +73,10 @@ describe('Result Decorator', () => { }, ]; - const decoratedResult: ScannerResults = resultDecorator.decorateResults( + const decoratedResults: DecoratedAxeResults = resultDecorator.decorateResults( resultStub, ); - expect(decoratedResult.violations).toEqual(expectedViolation); - expect(decoratedResult.violations[0].WCAG).toEqual([{ text: 'test' }]); + expect(decoratedResults.violations).toEqual(expectedViolation); + expect(decoratedResults.violations[0].WCAG).toEqual([{ text: 'test' }]); }); }); diff --git a/src/result-decorator.ts b/src/result-decorator.ts index 28e73f3b..f7e7f472 100644 --- a/src/result-decorator.ts +++ b/src/result-decorator.ts @@ -1,14 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as Axe from 'axe-core'; +import { + DecoratedAxeResult, + DecoratedAxeResults, +} from './decorated-axe-results'; import { DictionaryStringTo } from './dictionary-types'; -import { AxeCoreRuleResult, AxeRule, ScannerResults } from './ruleresults'; import { WCAG } from './wcag'; export class ResultDecorator { constructor(private wcagConfiguration: DictionaryStringTo) {} - public decorateResults(results: Axe.AxeResults): ScannerResults { + public decorateResults(results: Axe.AxeResults): DecoratedAxeResults { return { passes: this.decorateAxeRuleResults(results.passes), violations: this.decorateAxeRuleResults(results.violations), @@ -21,9 +24,9 @@ export class ResultDecorator { } private decorateAxeRuleResults( - ruleResults: AxeRule[], - ): AxeCoreRuleResult[] { - return ruleResults.map((result: AxeRule) => { + ruleResults: Axe.Result[], + ): DecoratedAxeResult[] { + return ruleResults.map((result: Axe.Result) => { return { ...result, WCAG: this.getRelatedWCAGByRuleId(result.id) }; }); } diff --git a/src/ruleresults.ts b/src/ruleresults.ts deleted file mode 100644 index 79cd1a2e..00000000 --- a/src/ruleresults.ts +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -import { WCAG } from './wcag'; - -export interface AxeRule { - id: string; - nodes: AxeNodeResult[]; - description: string; - help?: string; -} - -export interface AxeNodeResult { - any: FormattedCheckResult[]; - none: FormattedCheckResult[]; - all: FormattedCheckResult[]; - html: string; - target: string[]; // selector - failureSummary?: string; - instanceId?: string; - snippet?: string; -} - -export interface RulesConfiguration { - checks: CheckConfiguration[]; - rule: RuleConfiguration; -} - -export interface AxeConfiguration { - checks?: CheckConfiguration[]; - rules?: RuleConfiguration[]; -} - -export interface AxeBranding { - brand?: string; - application?: string; -} -export interface RuleConfiguration { - id: string; - selector: string; - excludeHidden?: boolean; - enabled?: boolean; - pageLevel?: boolean; - any?: string[]; - all?: string[]; - none?: string[]; - tags?: string[]; - matches?: (node: any, virtualNode: any) => boolean; - description?: string; - help?: string; - options?: any; - decorateNode?: (node: AxeNodeResult) => void; - helpUrl?: string; -} - -export interface CheckConfiguration { - id: string; - evaluate: ( - node: any, - options: any, - virtualNode: any, - context: any, - ) => boolean; - after?: any; - options?: any; - enabled?: boolean; - passMessage?: () => string; - failMessage?: () => string; -} - -export interface FormattedCheckResult { - id: string; - message: string; - data: AxeCheckResultExtraData; - result?: boolean; -} - -export interface AxeCheckResultExtraData { - headingLevel?: number; - headingText?: string; -} - -export interface AxeCheckResultFrameExtraData { - frameType?: string; - frameTitle?: string; -} - -export type AxeCoreRuleResult = AxeRule & AxeCoreDecorations; - -export interface ScannerResults { - passes: AxeCoreRuleResult[]; - violations: AxeCoreRuleResult[]; - inapplicable: AxeCoreRuleResult[]; - incomplete: AxeCoreRuleResult[]; - timestamp: string; - targetPageUrl: string; - targetPageTitle: string; -} - -export interface AxeCoreDecorations { - WCAG?: WCAG[]; - helpUrl?: string; -} diff --git a/src/sarif-converter.ts b/src/sarif-converter.ts index d4eaf781..1bee30e4 100644 --- a/src/sarif-converter.ts +++ b/src/sarif-converter.ts @@ -1,23 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as Axe from 'axe-core'; import { ConverterOptions } from './converter-options'; -import { DictionaryStringTo } from './dictionary-types'; import { - AxeCoreRuleResult, - AxeNodeResult, - FormattedCheckResult, - ScannerResults, -} from './ruleresults'; + DecoratedAxeResult, + DecoratedAxeResults, +} from './decorated-axe-results'; +import { DictionaryStringTo } from './dictionary-types'; import * as CustomSarif from './sarif/custom-sarif-types'; -import { SarifLog } from './sarif/sarifLog'; -import * as Sarif from './sarif/sarifv2'; +import * as Sarif from './sarif/sarif-2.0.0'; +import { SarifLog } from './sarif/sarif-log'; import { StringUtils } from './string-utils'; export class SarifConverter { constructor() {} public convert( - results: ScannerResults, + results: DecoratedAxeResults, options: ConverterOptions, ): SarifLog { return { @@ -27,7 +26,7 @@ export class SarifConverter { } private convertRun( - results: ScannerResults, + results: DecoratedAxeResults, options: ConverterOptions, ): Sarif.Run { const files: DictionaryStringTo = {}; @@ -83,7 +82,7 @@ export class SarifConverter { } private convertResults( - results: ScannerResults, + results: DecoratedAxeResults, properties: DictionaryStringTo, ): Sarif.Result[] { const resultArray: Sarif.Result[] = []; @@ -121,7 +120,7 @@ export class SarifConverter { private convertRuleResults( resultArray: Sarif.Result[], - ruleResults: AxeCoreRuleResult[], + ruleResults: DecoratedAxeResult[], level: CustomSarif.Result.level, targetPageUrl: string, properties: DictionaryStringTo, @@ -141,7 +140,7 @@ export class SarifConverter { private convertRuleResult( resultArray: Sarif.Result[], - ruleResult: AxeCoreRuleResult, + ruleResult: DecoratedAxeResult, level: CustomSarif.Result.level, targetPageUrl: string, properties: DictionaryStringTo, @@ -186,7 +185,7 @@ export class SarifConverter { } private getPartialFingerprintsFromRule( - ruleResult: AxeCoreRuleResult, + ruleResult: DecoratedAxeResult, ): DictionaryStringTo { return { ruleId: ruleResult.id, @@ -194,7 +193,7 @@ export class SarifConverter { } private convertMessage( - node: AxeNodeResult, + node: Axe.NodeResult, level: CustomSarif.Result.level, ): CustomSarif.Message { const textArray: string[] = []; @@ -232,7 +231,7 @@ export class SarifConverter { private convertMessageChecks( heading: string, - checkResults: FormattedCheckResult[], + checkResults: Axe.CheckResult[], textArray: string[], richTextArray: string[], ): void { @@ -263,7 +262,7 @@ export class SarifConverter { private convertRuleResultsWithoutNodes( resultArray: Sarif.Result[], - ruleResults: AxeCoreRuleResult[], + ruleResults: DecoratedAxeResult[], level: CustomSarif.Result.level, properties: DictionaryStringTo, ): void { @@ -286,7 +285,7 @@ export class SarifConverter { } private convertResultsToRules( - results: ScannerResults, + results: DecoratedAxeResults, ): DictionaryStringTo { const rulesDictionary: DictionaryStringTo = {}; @@ -300,7 +299,7 @@ export class SarifConverter { private convertRuleResultsToRules( rulesDictionary: DictionaryStringTo, - ruleResults: AxeCoreRuleResult[], + ruleResults: DecoratedAxeResult[], ): void { if (ruleResults) { for (const ruleResult of ruleResults) { @@ -311,7 +310,7 @@ export class SarifConverter { private convertRuleResultToRule( rulesDictionary: DictionaryStringTo, - ruleResult: AxeCoreRuleResult, + ruleResult: DecoratedAxeResult, ): void { if (!rulesDictionary.hasOwnProperty(ruleResult.id)) { const rule: Sarif.Rule = { @@ -329,10 +328,3 @@ export class SarifConverter { } } } - -export interface AxeCoreStandard { - standardName: CustomSarif.Message; - requirementName: CustomSarif.Message; - requirementId: string; - requirementUri: string; -} diff --git a/src/sarif/custom-sarif-types.ts b/src/sarif/custom-sarif-types.ts index f4c95ead..f0aa5641 100644 --- a/src/sarif/custom-sarif-types.ts +++ b/src/sarif/custom-sarif-types.ts @@ -18,7 +18,6 @@ export namespace Result { /** * Encapsulates a message intended to be read by the end user. */ -// tslint:disable-next-line:interface-name export interface Message { /** * A plain text message string. diff --git a/src/sarif/sarifv2.d.ts b/src/sarif/sarif-2.0.0.ts similarity index 100% rename from src/sarif/sarifv2.d.ts rename to src/sarif/sarif-2.0.0.ts diff --git a/src/sarif/sarifLog.ts b/src/sarif/sarif-log.ts similarity index 58% rename from src/sarif/sarifLog.ts rename to src/sarif/sarif-log.ts index e130b893..68a60589 100644 --- a/src/sarif/sarifLog.ts +++ b/src/sarif/sarif-log.ts @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { StaticAnalysisResultsFormatSarifVersion200JsonSchema } from './sarifv2'; -export interface SarifLog - extends StaticAnalysisResultsFormatSarifVersion200JsonSchema {} +import { StaticAnalysisResultsFormatSarifVersion200JsonSchema } from './sarif-2.0.0'; + +export type SarifLog = StaticAnalysisResultsFormatSarifVersion200JsonSchema;