From d565a106b76f2aa50c392f04545015f5fed962ac Mon Sep 17 00:00:00 2001 From: jdickson Date: Wed, 23 Oct 2024 11:05:07 +0700 Subject: [PATCH 1/3] Add Sarif parser --- src/Config/@enums/projectType.ts | 1 + src/Parser/SarifParser.spec.ts | 278 +++++++++++++++++++++++++++++++ src/Parser/SarifParser.ts | 132 +++++++++++++++ src/app.ts | 3 + 4 files changed, 414 insertions(+) create mode 100644 src/Parser/SarifParser.spec.ts create mode 100644 src/Parser/SarifParser.ts diff --git a/src/Config/@enums/projectType.ts b/src/Config/@enums/projectType.ts index 98d22ad..272d880 100644 --- a/src/Config/@enums/projectType.ts +++ b/src/Config/@enums/projectType.ts @@ -8,6 +8,7 @@ export enum ProjectType { androidlint = 'androidlint', dartlint = 'dartlint', swiftlint = 'swiftlint', + sarif = 'sarif', // copy paste detector jscpd = 'jscpd', diff --git a/src/Parser/SarifParser.spec.ts b/src/Parser/SarifParser.spec.ts new file mode 100644 index 0000000..cd3f735 --- /dev/null +++ b/src/Parser/SarifParser.spec.ts @@ -0,0 +1,278 @@ +import { LintSeverity } from './@enums/LintSeverity'; +import { LintItem } from './@types'; +import { SarifParser } from './SarifParser'; + +describe('SarifParser tests', () => { + const cwdWin = 'C:\\source'; + const cwdUnix = '/dir'; + + const basicSarifLog = { + version: '2.1.0', + runs: [ + { + tool: { + driver: { + name: 'TestAnalyzer', + rules: [ + { + id: 'TEST001', + shortDescription: { + text: 'Test rule description' + } + } + ] + } + }, + results: [ + { + ruleId: 'TEST001', + level: 'warning', + message: { + text: 'This is a test warning' + }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: 'C:\\source\\Test.cs' + }, + region: { + startLine: 42, + startColumn: 13 + } + } + } + ] + } + ] + } + ] + }; + + const sarifLogNoLocation = { + version: '2.1.0', + runs: [ + { + tool: { + driver: { + name: 'TestAnalyzer' + } + }, + results: [ + { + ruleId: 'TEST002', + level: 'error', + message: { + text: 'Error without location' + } + } + ] + } + ] + }; + + const sarifLogMultipleResults = { + version: '2.1.0', + runs: [ + { + tool: { + driver: { + name: 'TestAnalyzer' + } + }, + results: [ + { + ruleId: 'TEST003', + level: 'warning', + message: { + text: 'First warning' + }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: 'C:\\source\\Test1.cs' + }, + region: { + startLine: 10, + startColumn: 5 + } + } + } + ] + }, + { + ruleId: 'TEST004', + level: 'error', + message: { + text: 'Second error' + }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: 'C:\\source\\Test2.cs' + }, + region: { + startLine: 20, + startColumn: 8 + } + } + } + ] + } + ] + } + ] + }; + + const sarifLogUnrelatedPath = { + version: '2.1.0', + runs: [ + { + tool: { + driver: { + name: 'TestAnalyzer' + } + }, + results: [ + { + ruleId: 'TEST005', + level: 'warning', + message: { + text: 'Warning with unrelated path' + }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: '/usr/share/test/Unrelated.cs' + }, + region: { + startLine: 15, + startColumn: 3 + } + } + } + ] + } + ] + } + ] + }; + + it('Should parse basic SARIF log correctly', () => { + const result = new SarifParser(cwdWin).parse(JSON.stringify(basicSarifLog)); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + ruleId: 'TEST001', + source: 'Test.cs', + severity: LintSeverity.warning, + line: 42, + lineOffset: 13, + msg: 'TEST001: This is a test warning', + log: expect.any(String), + valid: true, + type: 'sarif', + } as LintItem); + }); + + it('Should handle results without location information', () => { + const result = new SarifParser(cwdWin).parse(JSON.stringify(sarifLogNoLocation)); + expect(result).toHaveLength(0); + }); + + it('Should parse multiple results correctly', () => { + const result = new SarifParser(cwdWin).parse(JSON.stringify(sarifLogMultipleResults)); + expect(result).toHaveLength(2); + expect(result[0].severity).toBe(LintSeverity.warning); + expect(result[1].severity).toBe(LintSeverity.error); + expect(result[0].source).toBe('Test1.cs'); + expect(result[1].source).toBe('Test2.cs'); + }); + + it('Should handle unrelated paths correctly and flag as invalid', () => { + const result = new SarifParser(cwdUnix).parse(JSON.stringify(sarifLogUnrelatedPath)); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + ruleId: 'TEST005', + source: 'Unrelated.cs', + severity: LintSeverity.warning, + line: 15, + lineOffset: 3, + msg: 'TEST005: Warning with unrelated path', + log: expect.any(String), + valid: false, + type: 'sarif', + } as LintItem); + }); + + it('Should handle empty SARIF log', () => { + const emptyLog = { + version: '2.1.0', + runs: [ + { + tool: { + driver: { + name: 'TestAnalyzer' + } + }, + results: [] + } + ] + }; + const result = new SarifParser(cwdWin).parse(JSON.stringify(emptyLog)); + expect(result).toHaveLength(0); + }); + + it('Should throw error on invalid JSON', () => { + expect(() => new SarifParser(cwdWin).parse('{')).toThrowError(); + }); + + it('Should throw error on invalid SARIF format', () => { + const invalidLog = { + version: '2.1.0', + // missing runs array + }; + expect(() => new SarifParser(cwdWin).parse(JSON.stringify(invalidLog))).toThrowError(); + }); + + it('Should handle missing severity level and default to warning', () => { + const logWithNoLevel = { + version: '2.1.0', + runs: [ + { + tool: { + driver: { + name: 'TestAnalyzer' + } + }, + results: [ + { + ruleId: 'TEST006', + message: { + text: 'Message with no severity level' + }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: 'C:\\source\\Test.cs' + }, + region: { + startLine: 1, + startColumn: 1 + } + } + } + ] + } + ] + } + ] + }; + const result = new SarifParser(cwdWin).parse(JSON.stringify(logWithNoLevel)); + expect(result).toHaveLength(1); + expect(result[0].severity).toBe(LintSeverity.warning); + }); +}); \ No newline at end of file diff --git a/src/Parser/SarifParser.ts b/src/Parser/SarifParser.ts new file mode 100644 index 0000000..6e6f723 --- /dev/null +++ b/src/Parser/SarifParser.ts @@ -0,0 +1,132 @@ +import { basename } from 'path'; +import slash from 'slash'; + +import { Log } from '../Logger'; +import { getRelativePath } from './utils/path.util'; +import { Parser } from './@interfaces/parser.interface'; +import { LintItem } from './@types'; +import { mapSeverity } from './utils/dotnetSeverityMap'; +import { ProjectType } from '../Config/@enums'; +import { NoNaN } from './utils/number.util'; + +interface SarifLog { + version: string; + runs: SarifRun[]; +} + +interface SarifRun { + tool: { + driver: { + name: string; + rules?: SarifRule[]; + }; + }; + results: SarifResult[]; +} + +interface SarifRule { + id: string; + shortDescription?: { + text: string; + }; +} + +interface SarifResult { + ruleId: string; + level?: 'none' | 'note' | 'warning' | 'error'; + message: { + text: string; + }; + locations?: Array<{ + physicalLocation: { + artifactLocation: { + uri: string; + }; + region?: { + startLine: number; + startColumn?: number; + }; + }; + }>; +} + +export class SarifParser extends Parser { + parse(content: string): LintItem[] { + try { + const sarifLog: SarifLog = JSON.parse(content); + + if (!this.isValidSarifLog(sarifLog)) { + const message = "SarifParser Error: Invalid SARIF format"; + Log.error(message, { content }); + throw new Error(message); + } + + const lintItems: LintItem[] = []; + + for (const run of sarifLog.runs) { + const results = run.results || []; + for (const result of results) { + const lintItem = this.toLintItem(result, run); + if (lintItem) { + lintItems.push(lintItem); + } + } + } + + return lintItems; + } catch (error) { + const message = "SarifParser Error: Failed to parse SARIF content"; + Log.error(message, { error, content }); + throw new Error(message); + } + } + + private isValidSarifLog(log: any): log is SarifLog { + return ( + log && + typeof log === 'object' && + typeof log.version === 'string' && + Array.isArray(log.runs) + ); + } + + private toLintItem(result: SarifResult, run: SarifRun): LintItem | null { + if (!result.locations?.[0]) { + Log.warn('SarifParser Warning: Result has no location information', { result }); + return null; + } + + const location = result.locations[0].physicalLocation; + const uri = location.artifactLocation.uri; + const relativeSrcPath = getRelativePath(this.cwd, uri); + + if (!relativeSrcPath) { + Log.warn(`SarifParser Warning: source path is not relative to root`, { uri }); + } + + // Map SARIF severity levels to your existing severity system + const severityMap: Record = { + 'error': 'error', + 'warning': 'warning', + 'note': 'info', + 'none': 'info' + }; + + return { + ruleId: result.ruleId, + log: JSON.stringify(result), // Store the original result for reference + line: NoNaN(location.region?.startLine), + lineOffset: NoNaN(location.region?.startColumn), + msg: `${result.ruleId}: ${result.message.text}`, + source: relativeSrcPath ?? basename(slash(uri)), + severity: mapSeverity(severityMap[result.level ?? 'warning']), + valid: !!relativeSrcPath, + type: ProjectType.sarif, + }; + } + + // Helper method to find rule details if needed + private findRuleDetails(ruleId: string, run: SarifRun): SarifRule | undefined { + return run.tool.driver.rules?.find(rule => rule.id === ruleId); + } +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 9132fc2..58277be 100644 --- a/src/app.ts +++ b/src/app.ts @@ -28,6 +28,7 @@ import { gitLabFormatter, OutputFormatter, } from './OutputFormatter/OutputFormatter'; +import { SarifParser } from './Parser/SarifParser'; class App { private vcs: VCS | null = null; @@ -91,6 +92,8 @@ class App { return new SwiftLintParser(cwd); case ProjectType.jscpd: return new JscpdParser(cwd); + case ProjectType.sarif: + return new SarifParser(cwd); } } From c76264827662303ea35f0371a1c2d91e4bba9828 Mon Sep 17 00:00:00 2001 From: jdickson Date: Wed, 23 Oct 2024 11:10:57 +0700 Subject: [PATCH 2/3] fix errors --- src/Parser/SarifParser.spec.ts | 156 +++++++++++++++++---------------- src/Parser/SarifParser.ts | 90 +++++++++++-------- 2 files changed, 132 insertions(+), 114 deletions(-) diff --git a/src/Parser/SarifParser.spec.ts b/src/Parser/SarifParser.spec.ts index cd3f735..d821eb7 100644 --- a/src/Parser/SarifParser.spec.ts +++ b/src/Parser/SarifParser.spec.ts @@ -17,36 +17,36 @@ describe('SarifParser tests', () => { { id: 'TEST001', shortDescription: { - text: 'Test rule description' - } - } - ] - } + text: 'Test rule description', + }, + }, + ], + }, }, results: [ { ruleId: 'TEST001', level: 'warning', message: { - text: 'This is a test warning' + text: 'This is a test warning', }, locations: [ { physicalLocation: { artifactLocation: { - uri: 'C:\\source\\Test.cs' + uri: 'C:\\source\\Test.cs', }, region: { startLine: 42, - startColumn: 13 - } - } - } - ] - } - ] - } - ] + startColumn: 13, + }, + }, + }, + ], + }, + ], + }, + ], }; const sarifLogNoLocation = { @@ -55,20 +55,20 @@ describe('SarifParser tests', () => { { tool: { driver: { - name: 'TestAnalyzer' - } + name: 'TestAnalyzer', + }, }, results: [ { ruleId: 'TEST002', level: 'error', message: { - text: 'Error without location' - } - } - ] - } - ] + text: 'Error without location', + }, + }, + ], + }, + ], }; const sarifLogMultipleResults = { @@ -77,53 +77,53 @@ describe('SarifParser tests', () => { { tool: { driver: { - name: 'TestAnalyzer' - } + name: 'TestAnalyzer', + }, }, results: [ { ruleId: 'TEST003', level: 'warning', message: { - text: 'First warning' + text: 'First warning', }, locations: [ { physicalLocation: { artifactLocation: { - uri: 'C:\\source\\Test1.cs' + uri: 'C:\\source\\Test1.cs', }, region: { startLine: 10, - startColumn: 5 - } - } - } - ] + startColumn: 5, + }, + }, + }, + ], }, { ruleId: 'TEST004', level: 'error', message: { - text: 'Second error' + text: 'Second error', }, locations: [ { physicalLocation: { artifactLocation: { - uri: 'C:\\source\\Test2.cs' + uri: 'C:\\source\\Test2.cs', }, region: { startLine: 20, - startColumn: 8 - } - } - } - ] - } - ] - } - ] + startColumn: 8, + }, + }, + }, + ], + }, + ], + }, + ], }; const sarifLogUnrelatedPath = { @@ -132,33 +132,33 @@ describe('SarifParser tests', () => { { tool: { driver: { - name: 'TestAnalyzer' - } + name: 'TestAnalyzer', + }, }, results: [ { ruleId: 'TEST005', level: 'warning', message: { - text: 'Warning with unrelated path' + text: 'Warning with unrelated path', }, locations: [ { physicalLocation: { artifactLocation: { - uri: '/usr/share/test/Unrelated.cs' + uri: '/usr/share/test/Unrelated.cs', }, region: { startLine: 15, - startColumn: 3 - } - } - } - ] - } - ] - } - ] + startColumn: 3, + }, + }, + }, + ], + }, + ], + }, + ], }; it('Should parse basic SARIF log correctly', () => { @@ -214,12 +214,12 @@ describe('SarifParser tests', () => { { tool: { driver: { - name: 'TestAnalyzer' - } + name: 'TestAnalyzer', + }, }, - results: [] - } - ] + results: [], + }, + ], }; const result = new SarifParser(cwdWin).parse(JSON.stringify(emptyLog)); expect(result).toHaveLength(0); @@ -234,7 +234,9 @@ describe('SarifParser tests', () => { version: '2.1.0', // missing runs array }; - expect(() => new SarifParser(cwdWin).parse(JSON.stringify(invalidLog))).toThrowError(); + expect(() => + new SarifParser(cwdWin).parse(JSON.stringify(invalidLog)), + ).toThrowError(); }); it('Should handle missing severity level and default to warning', () => { @@ -244,35 +246,35 @@ describe('SarifParser tests', () => { { tool: { driver: { - name: 'TestAnalyzer' - } + name: 'TestAnalyzer', + }, }, results: [ { ruleId: 'TEST006', message: { - text: 'Message with no severity level' + text: 'Message with no severity level', }, locations: [ { physicalLocation: { artifactLocation: { - uri: 'C:\\source\\Test.cs' + uri: 'C:\\source\\Test.cs', }, region: { startLine: 1, - startColumn: 1 - } - } - } - ] - } - ] - } - ] + startColumn: 1, + }, + }, + }, + ], + }, + ], + }, + ], }; const result = new SarifParser(cwdWin).parse(JSON.stringify(logWithNoLevel)); expect(result).toHaveLength(1); expect(result[0].severity).toBe(LintSeverity.warning); }); -}); \ No newline at end of file +}); diff --git a/src/Parser/SarifParser.ts b/src/Parser/SarifParser.ts index 6e6f723..3e69e34 100644 --- a/src/Parser/SarifParser.ts +++ b/src/Parser/SarifParser.ts @@ -7,6 +7,7 @@ import { Parser } from './@interfaces/parser.interface'; import { LintItem } from './@types'; import { mapSeverity } from './utils/dotnetSeverityMap'; import { ProjectType } from '../Config/@enums'; +import { LintSeverity } from './@enums/LintSeverity'; import { NoNaN } from './utils/number.util'; interface SarifLog { @@ -53,34 +54,47 @@ interface SarifResult { export class SarifParser extends Parser { parse(content: string): LintItem[] { try { - const sarifLog: SarifLog = JSON.parse(content); - - if (!this.isValidSarifLog(sarifLog)) { - const message = "SarifParser Error: Invalid SARIF format"; - Log.error(message, { content }); - throw new Error(message); - } - - const lintItems: LintItem[] = []; - - for (const run of sarifLog.runs) { - const results = run.results || []; - for (const result of results) { - const lintItem = this.toLintItem(result, run); - if (lintItem) { - lintItems.push(lintItem); - } - } - } - - return lintItems; + const sarifLog = this.parseSarifContent(content); + return this.processRuns(sarifLog.runs); } catch (error) { - const message = "SarifParser Error: Failed to parse SARIF content"; + const message = 'SarifParser Error: Failed to parse SARIF content'; Log.error(message, { error, content }); throw new Error(message); } } + private parseSarifContent(content: string): SarifLog { + const sarifLog: SarifLog = JSON.parse(content); + + if (!this.isValidSarifLog(sarifLog)) { + const message = 'SarifParser Error: Invalid SARIF format'; + Log.error(message, { content }); + throw new Error(message); + } + + return sarifLog; + } + + private processRuns(runs: SarifRun[]): LintItem[] { + const lintItems: LintItem[] = []; + + for (const run of runs) { + this.processResults(run, lintItems); + } + + return lintItems; + } + + private processResults(run: SarifRun, lintItems: LintItem[]): void { + const results = run.results || []; + for (const result of results) { + const lintItem = this.toLintItem(result, run); + if (lintItem) { + lintItems.push(lintItem); + } + } + } + private isValidSarifLog(log: any): log is SarifLog { return ( log && @@ -104,29 +118,31 @@ export class SarifParser extends Parser { Log.warn(`SarifParser Warning: source path is not relative to root`, { uri }); } - // Map SARIF severity levels to your existing severity system - const severityMap: Record = { - 'error': 'error', - 'warning': 'warning', - 'note': 'info', - 'none': 'info' - }; - return { ruleId: result.ruleId, - log: JSON.stringify(result), // Store the original result for reference - line: NoNaN(location.region?.startLine), - lineOffset: NoNaN(location.region?.startColumn), + log: JSON.stringify(result), + line: NoNaN(String(location.region?.startLine || '')), + lineOffset: NoNaN(String(location.region?.startColumn || '')), msg: `${result.ruleId}: ${result.message.text}`, source: relativeSrcPath ?? basename(slash(uri)), - severity: mapSeverity(severityMap[result.level ?? 'warning']), + severity: this.getSeverity(result.level), valid: !!relativeSrcPath, type: ProjectType.sarif, }; } - // Helper method to find rule details if needed + private getSeverity(level?: string): LintSeverity { + const severityMap: Record = { + error: 'error', + warning: 'warning', + note: 'info', + none: 'info', + }; + + return mapSeverity(severityMap[level ?? 'warning']); + } + private findRuleDetails(ruleId: string, run: SarifRun): SarifRule | undefined { - return run.tool.driver.rules?.find(rule => rule.id === ruleId); + return run.tool.driver.rules?.find((rule) => rule.id === ruleId); } -} \ No newline at end of file +} From e283b3e6c25daf41d5d3d58f4d20e12a8517a231 Mon Sep 17 00:00:00 2001 From: Joel Dickson <9032274+joeldickson@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:21:29 +0700 Subject: [PATCH 3/3] Update README.md --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 07b3a8c..c549366 100644 --- a/README.md +++ b/README.md @@ -155,5 +155,19 @@ Use `-o ` on output lint result created by command `dart analyze > ` to output lint result to file and `--reporter json` to format logs as JSON. (_[ref.](https://github.com/realm/SwiftLint#command-line)_) +#### Kotlin Detekt +In gradle config +(_[ref.](https://detekt.dev/docs/gettingstarted/gradle#reports)_) + +```kotlin +tasks.named("detekt").configure { + reports { + // Enable/Disable SARIF report (default: false) + sarif.required.set(true) + sarif.outputLocation.set(file("build/reports/detekt.sarif")) + } +} +``` + ### Contribute For contribution guidelines and project dev setup. Please see [CONTRIBUTING.md](CONTRIBUTING.md)