@owasp-aghast/aghast 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/config/pricing.json +39 -9
- package/dist/build-config.js +1 -1
- package/dist/check-library.d.ts.map +1 -1
- package/dist/check-library.js +7 -1
- package/dist/check-library.js.map +1 -1
- package/dist/claude-code-provider.d.ts +7 -0
- package/dist/claude-code-provider.d.ts.map +1 -1
- package/dist/claude-code-provider.js +65 -1
- package/dist/claude-code-provider.js.map +1 -1
- package/dist/discoveries/opengrep-discovery.d.ts +10 -0
- package/dist/discoveries/opengrep-discovery.d.ts.map +1 -0
- package/dist/discoveries/opengrep-discovery.js +41 -0
- package/dist/discoveries/opengrep-discovery.js.map +1 -0
- package/dist/discoveries/sarif-target-enrichment.d.ts +7 -0
- package/dist/discoveries/sarif-target-enrichment.d.ts.map +1 -0
- package/dist/discoveries/sarif-target-enrichment.js +28 -0
- package/dist/discoveries/sarif-target-enrichment.js.map +1 -0
- package/dist/discoveries/semgrep-discovery.d.ts.map +1 -1
- package/dist/discoveries/semgrep-discovery.js +2 -23
- package/dist/discoveries/semgrep-discovery.js.map +1 -1
- package/dist/error-codes.d.ts +3 -1
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +4 -2
- package/dist/error-codes.js.map +1 -1
- package/dist/formatters/csv-formatter.d.ts +34 -0
- package/dist/formatters/csv-formatter.d.ts.map +1 -0
- package/dist/formatters/csv-formatter.js +114 -0
- package/dist/formatters/csv-formatter.js.map +1 -0
- package/dist/formatters/html-formatter.d.ts +39 -0
- package/dist/formatters/html-formatter.d.ts.map +1 -0
- package/dist/formatters/html-formatter.js +392 -0
- package/dist/formatters/html-formatter.js.map +1 -0
- package/dist/formatters/index.d.ts.map +1 -1
- package/dist/formatters/index.js +4 -0
- package/dist/formatters/index.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +41 -5
- package/dist/index.js.map +1 -1
- package/dist/new-check.d.ts.map +1 -1
- package/dist/new-check.js +37 -21
- package/dist/new-check.js.map +1 -1
- package/dist/opengrep-runner.d.ts +21 -0
- package/dist/opengrep-runner.d.ts.map +1 -0
- package/dist/opengrep-runner.js +31 -0
- package/dist/opengrep-runner.js.map +1 -0
- package/dist/scan-runner.d.ts.map +1 -1
- package/dist/scan-runner.js +2 -0
- package/dist/scan-runner.js.map +1 -1
- package/dist/semgrep-runner.d.ts +50 -4
- package/dist/semgrep-runner.d.ts.map +1 -1
- package/dist/semgrep-runner.js +60 -26
- package/dist/semgrep-runner.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semgrep-discovery.js","sourceRoot":"","sources":["../../src/discoveries/semgrep-discovery.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"semgrep-discovery.js","sourceRoot":"","sources":["../../src/discoveries/semgrep-discovery.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,gCAAgC,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAIxD,MAAM,GAAG,GAAG,mBAAmB,CAAC;AAEhC,MAAM,CAAC,MAAM,gBAAgB,GAAoB;IAC/C,IAAI,EAAE,SAAS;IACf,oBAAoB,EAAE,sBAAsB;IAC5C,iBAAiB,EAAE,IAAI;IACvB,kBAAkB,EAAE,IAAI;IAExB,KAAK,CAAC,QAAQ,CACZ,KAAoB,EACpB,QAAgB,EAChB,QAA2B;QAE3B,MAAM,WAAW,GAAG,KAAK,CAAC,WAAY,CAAC;QAEvC,QAAQ,CAAC,GAAG,EAAE,8BAA8B,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QAExD,MAAM,YAAY,GAAG,MAAM,UAAU,CAAC;YACpC,cAAc,EAAE,QAAQ;YACxB,KAAK,EAAE,WAAW,CAAC,KAAK;YACxB,MAAM,EAAE,WAAW,CAAC,MAAM;SAC3B,CAAC,CAAC;QAEH,IAAI,OAAO,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC;QACvC,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAEtC,QAAQ,CAAC,GAAG,EAAE,cAAc,OAAO,CAAC,MAAM,UAAU,CAAC,CAAC;QAEtD,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;YACnC,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,KAAK,EAAE,WAAW,GAAG,GAAG,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG;YAC9C,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,gBAAgB,EAAE,gCAAgC,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC;SAClG,CAAC,CAAC,CAAC;IACN,CAAC;CACF,CAAC"}
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* E2xxx — Configuration (config dir, checks, runtime config)
|
|
7
7
|
* E3xxx — Agent provider
|
|
8
8
|
* E4xxx — Repository/target validation
|
|
9
|
-
* E5xxx — Semgrep
|
|
9
|
+
* E5xxx — Semgrep / Opengrep
|
|
10
10
|
* E6xxx — OpenAnt
|
|
11
|
+
* E7xxx — Budget / cost controls
|
|
11
12
|
* E9xxx — Internal/fatal
|
|
12
13
|
*/
|
|
13
14
|
export interface ErrorCode {
|
|
@@ -29,6 +30,7 @@ export declare const ERROR_CODES: {
|
|
|
29
30
|
readonly E3004: ErrorCode;
|
|
30
31
|
readonly E4001: ErrorCode;
|
|
31
32
|
readonly E5001: ErrorCode;
|
|
33
|
+
readonly E5101: ErrorCode;
|
|
32
34
|
readonly E6001: ErrorCode;
|
|
33
35
|
readonly E6002: ErrorCode;
|
|
34
36
|
readonly E7001: ErrorCode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-codes.d.ts","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"error-codes.d.ts","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAMD,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;CAuCd,CAAC;AAEX;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAWzE"}
|
package/dist/error-codes.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* E2xxx — Configuration (config dir, checks, runtime config)
|
|
7
7
|
* E3xxx — Agent provider
|
|
8
8
|
* E4xxx — Repository/target validation
|
|
9
|
-
* E5xxx — Semgrep
|
|
9
|
+
* E5xxx — Semgrep / Opengrep
|
|
10
10
|
* E6xxx — OpenAnt
|
|
11
|
+
* E7xxx — Budget / cost controls
|
|
11
12
|
* E9xxx — Internal/fatal
|
|
12
13
|
*/
|
|
13
14
|
function ec(code, label) {
|
|
@@ -35,8 +36,9 @@ export const ERROR_CODES = {
|
|
|
35
36
|
E3004: ec('E3004', 'OpenCode not installed'),
|
|
36
37
|
// E4xxx — Repository/target validation
|
|
37
38
|
E4001: ec('E4001', 'Repository path not found'),
|
|
38
|
-
// E5xxx — Semgrep
|
|
39
|
+
// E5xxx — Semgrep / Opengrep
|
|
39
40
|
E5001: ec('E5001', 'Semgrep not installed'),
|
|
41
|
+
E5101: ec('E5101', 'Opengrep not installed'),
|
|
40
42
|
// E6xxx — OpenAnt
|
|
41
43
|
E6001: ec('E6001', 'OpenAnt not installed'),
|
|
42
44
|
E6002: ec('E6002', 'OpenAnt execution failed'),
|
package/dist/error-codes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-codes.js","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"error-codes.js","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,SAAS,EAAE,CAAC,IAAY,EAAE,KAAa;IACrC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,sBAAsB;IACtB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,gCAAgC,CAAC;IACpD,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAE5C,wBAAwB;IACxB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,gCAAgC,CAAC;IACpD,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,oCAAoC,CAAC;IACxD,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,0BAA0B,CAAC;IAC9C,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,qBAAqB,CAAC;IAEzC,yBAAyB;IACzB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACrC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAC5C,yEAAyE;IACzE,wEAAwE;IACxE,qEAAqE;IACrE,oEAAoE;IACpE,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,iCAAiC,CAAC;IACrD,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAE5C,uCAAuC;IACvC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,2BAA2B,CAAC;IAE/C,6BAA6B;IAC7B,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAC3C,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAE5C,kBAAkB;IAClB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAC3C,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,0BAA0B,CAAC;IAE9C,iCAAiC;IACjC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAE3C,mBAAmB;IACnB,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,sBAAsB,CAAC;CAClC,CAAC;AAEX;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,SAAoB,EAAE,OAAe;IAC/D,OAAO,UAAU,SAAS,CAAC,IAAI,MAAM,OAAO,EAAE,CAAC;AACjD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,OAAe;IAC/D,MAAM,KAAK,GAAG,kBAAkB,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,kBAAkB,CAAC,gBAAgB,OAAO,mBAAmB,OAAO,EAAE,CAAC,CAAC;IACrF,MAAM,GAAG,GAAG,2DAA2D,KAAK,SAAS,IAAI,aAAa,CAAC;IACvG,OAAO;QACL,uBAAuB,WAAW,CAAC,KAAK,CAAC,IAAI,MAAM,OAAO,EAAE;QAC5D,YAAY,OAAO,EAAE;QACrB,EAAE;QACF,0CAA0C;QAC1C,KAAK,GAAG,EAAE;KACX,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV output formatter — one row per SecurityIssue (plus a row per ERROR check)
|
|
3
|
+
* for spreadsheet analysis (Excel, Google Sheets, etc.).
|
|
4
|
+
*
|
|
5
|
+
* Output contract:
|
|
6
|
+
* - Encoding: UTF-8, no BOM. Excel-on-Windows may render non-ASCII bytes as
|
|
7
|
+
* mojibake when a CSV is opened directly without an encoding hint; consumers
|
|
8
|
+
* that need Excel auto-detection should use the JSON or SARIF output instead,
|
|
9
|
+
* or import the CSV via Excel's "From Text/CSV" wizard which lets you pick
|
|
10
|
+
* UTF-8 explicitly.
|
|
11
|
+
* - Line terminator: CRLF (RFC 4180). Final row is also CRLF-terminated.
|
|
12
|
+
* - Quoting: RFC 4180 — fields containing `,`, `"`, CR, or LF are wrapped in
|
|
13
|
+
* double quotes; embedded `"` is doubled.
|
|
14
|
+
* - Description: flattened to a single line (CR/LF/CRLF → single space) and
|
|
15
|
+
* truncated to {@link DESCRIPTION_MAX_LENGTH} chars with a U+2026 ellipsis.
|
|
16
|
+
* - The structured `dataFlow` taint trace on `SecurityIssue` is intentionally
|
|
17
|
+
* omitted — flat per-issue rows can't represent a list well; SARIF/JSON
|
|
18
|
+
* output keeps the full trace.
|
|
19
|
+
*/
|
|
20
|
+
import type { ScanResults } from '../types.js';
|
|
21
|
+
import type { OutputFormatter } from './types.js';
|
|
22
|
+
/** Escapes a single CSV field per RFC 4180. */
|
|
23
|
+
export declare function escapeCsvField(value: string | number | undefined): string;
|
|
24
|
+
/**
|
|
25
|
+
* Truncates a description to fit within the CSV column and replaces newlines
|
|
26
|
+
* with spaces so each issue stays on a single row.
|
|
27
|
+
*/
|
|
28
|
+
export declare function normalizeDescription(description: string): string;
|
|
29
|
+
export declare class CsvFormatter implements OutputFormatter {
|
|
30
|
+
readonly id = "csv";
|
|
31
|
+
readonly fileExtension = ".csv";
|
|
32
|
+
format(results: ScanResults): string;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=csv-formatter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv-formatter.d.ts","sourceRoot":"","sources":["../../src/formatters/csv-formatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAwC,MAAM,aAAa,CAAC;AACrF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAkBlD,+CAA+C;AAC/C,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAQzE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAMhE;AAyCD,qBAAa,YAAa,YAAW,eAAe;IAClD,QAAQ,CAAC,EAAE,SAAS;IACpB,QAAQ,CAAC,aAAa,UAAU;IAEhC,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM;CAqBrC"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV output formatter — one row per SecurityIssue (plus a row per ERROR check)
|
|
3
|
+
* for spreadsheet analysis (Excel, Google Sheets, etc.).
|
|
4
|
+
*
|
|
5
|
+
* Output contract:
|
|
6
|
+
* - Encoding: UTF-8, no BOM. Excel-on-Windows may render non-ASCII bytes as
|
|
7
|
+
* mojibake when a CSV is opened directly without an encoding hint; consumers
|
|
8
|
+
* that need Excel auto-detection should use the JSON or SARIF output instead,
|
|
9
|
+
* or import the CSV via Excel's "From Text/CSV" wizard which lets you pick
|
|
10
|
+
* UTF-8 explicitly.
|
|
11
|
+
* - Line terminator: CRLF (RFC 4180). Final row is also CRLF-terminated.
|
|
12
|
+
* - Quoting: RFC 4180 — fields containing `,`, `"`, CR, or LF are wrapped in
|
|
13
|
+
* double quotes; embedded `"` is doubled.
|
|
14
|
+
* - Description: flattened to a single line (CR/LF/CRLF → single space) and
|
|
15
|
+
* truncated to {@link DESCRIPTION_MAX_LENGTH} chars with a U+2026 ellipsis.
|
|
16
|
+
* - The structured `dataFlow` taint trace on `SecurityIssue` is intentionally
|
|
17
|
+
* omitted — flat per-issue rows can't represent a list well; SARIF/JSON
|
|
18
|
+
* output keeps the full trace.
|
|
19
|
+
*/
|
|
20
|
+
/** Maximum length of the description field in CSV output (longer values are truncated with an ellipsis). */
|
|
21
|
+
const DESCRIPTION_MAX_LENGTH = 500;
|
|
22
|
+
const CSV_HEADERS = [
|
|
23
|
+
'checkId',
|
|
24
|
+
'checkName',
|
|
25
|
+
'status',
|
|
26
|
+
'file',
|
|
27
|
+
'startLine',
|
|
28
|
+
'endLine',
|
|
29
|
+
'severity',
|
|
30
|
+
'confidence',
|
|
31
|
+
'description',
|
|
32
|
+
'recommendation',
|
|
33
|
+
];
|
|
34
|
+
/** Escapes a single CSV field per RFC 4180. */
|
|
35
|
+
export function escapeCsvField(value) {
|
|
36
|
+
if (value === undefined || value === null)
|
|
37
|
+
return '';
|
|
38
|
+
const str = String(value);
|
|
39
|
+
// Wrap in quotes if it contains a comma, quote, CR, or LF.
|
|
40
|
+
if (/[",\r\n]/.test(str)) {
|
|
41
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
42
|
+
}
|
|
43
|
+
return str;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Truncates a description to fit within the CSV column and replaces newlines
|
|
47
|
+
* with spaces so each issue stays on a single row.
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeDescription(description) {
|
|
50
|
+
// Replace any sequence of CR/LF with a single space so the cell stays on one line
|
|
51
|
+
// (CSV viewers do support multi-line cells via quoting, but flattening keeps spreadsheets readable).
|
|
52
|
+
const flattened = description.replace(/\r\n|\r|\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
53
|
+
if (flattened.length <= DESCRIPTION_MAX_LENGTH)
|
|
54
|
+
return flattened;
|
|
55
|
+
return flattened.slice(0, DESCRIPTION_MAX_LENGTH - 1) + '…'; // ellipsis
|
|
56
|
+
}
|
|
57
|
+
function statusForIssue(issue, checkStatusByCheckId) {
|
|
58
|
+
// Fall back to 'UNKNOWN' (not 'FAIL') for orphaned issues so a row in the
|
|
59
|
+
// spreadsheet doesn't claim a check status that wasn't actually reported.
|
|
60
|
+
return checkStatusByCheckId.get(issue.checkId) ?? 'UNKNOWN';
|
|
61
|
+
}
|
|
62
|
+
function issueRow(issue, checkStatus) {
|
|
63
|
+
return [
|
|
64
|
+
escapeCsvField(issue.checkId),
|
|
65
|
+
escapeCsvField(issue.checkName),
|
|
66
|
+
escapeCsvField(checkStatus),
|
|
67
|
+
escapeCsvField(issue.file),
|
|
68
|
+
escapeCsvField(issue.startLine),
|
|
69
|
+
escapeCsvField(issue.endLine),
|
|
70
|
+
escapeCsvField(issue.severity),
|
|
71
|
+
escapeCsvField(issue.confidence),
|
|
72
|
+
escapeCsvField(normalizeDescription(issue.description)),
|
|
73
|
+
escapeCsvField(issue.recommendation),
|
|
74
|
+
].join(',');
|
|
75
|
+
}
|
|
76
|
+
function errorCheckRow(check) {
|
|
77
|
+
// Surface execution errors so they show up alongside issues in spreadsheets.
|
|
78
|
+
// No file/line/severity/confidence/recommendation — just the error in the description column.
|
|
79
|
+
const description = normalizeDescription(check.error ? `Check execution error: ${check.error}` : 'Check execution error');
|
|
80
|
+
return [
|
|
81
|
+
escapeCsvField(check.checkId),
|
|
82
|
+
escapeCsvField(check.checkName),
|
|
83
|
+
escapeCsvField(check.status),
|
|
84
|
+
'', // file
|
|
85
|
+
'', // startLine
|
|
86
|
+
'', // endLine
|
|
87
|
+
'', // severity
|
|
88
|
+
'', // confidence
|
|
89
|
+
escapeCsvField(description),
|
|
90
|
+
'', // recommendation
|
|
91
|
+
].join(',');
|
|
92
|
+
}
|
|
93
|
+
export class CsvFormatter {
|
|
94
|
+
id = 'csv';
|
|
95
|
+
fileExtension = '.csv';
|
|
96
|
+
format(results) {
|
|
97
|
+
const checkStatusByCheckId = new Map();
|
|
98
|
+
for (const check of results.checks) {
|
|
99
|
+
checkStatusByCheckId.set(check.checkId, check.status);
|
|
100
|
+
}
|
|
101
|
+
const lines = [CSV_HEADERS.join(',')];
|
|
102
|
+
for (const issue of results.issues) {
|
|
103
|
+
lines.push(issueRow(issue, statusForIssue(issue, checkStatusByCheckId)));
|
|
104
|
+
}
|
|
105
|
+
for (const check of results.checks) {
|
|
106
|
+
if (check.status === 'ERROR') {
|
|
107
|
+
lines.push(errorCheckRow(check));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// RFC 4180 specifies CRLF line endings.
|
|
111
|
+
return lines.join('\r\n') + '\r\n';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=csv-formatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv-formatter.js","sourceRoot":"","sources":["../../src/formatters/csv-formatter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML output formatter — self-contained interactive report.
|
|
3
|
+
*
|
|
4
|
+
* Emits a single .html file with inline CSS + JS. Includes:
|
|
5
|
+
* - Header with title, scan timestamp, repository info
|
|
6
|
+
* - Summary statistics (totals + per-status counters)
|
|
7
|
+
* - Filterable, severity-badged issues table
|
|
8
|
+
* - Expandable per-check sections with optional code snippets
|
|
9
|
+
* - The full `ScanResults` embedded as a JSON island for client-side filtering
|
|
10
|
+
*
|
|
11
|
+
* All user-controlled strings are HTML-escaped via `escapeHtml`. The embedded
|
|
12
|
+
* JSON has `</` sequences neutralised so an attacker-controlled string cannot
|
|
13
|
+
* break out of the `<script type="application/json">` tag.
|
|
14
|
+
*/
|
|
15
|
+
import type { ScanResults } from '../types.js';
|
|
16
|
+
import type { OutputFormatter } from './types.js';
|
|
17
|
+
/** Escapes the five HTML special characters. */
|
|
18
|
+
export declare function escapeHtml(value: unknown): string;
|
|
19
|
+
/**
|
|
20
|
+
* Embeds JSON inside a `<script type="application/json">` block safely.
|
|
21
|
+
*
|
|
22
|
+
* Two patterns matter for HTML's script-data tokenizer:
|
|
23
|
+
* 1. `</` — replaced with `<\/` so a stray `</script>` cannot terminate the
|
|
24
|
+
* script element early. `\/` is valid JSON for `/`, so JSON.parse
|
|
25
|
+
* round-trips faithfully.
|
|
26
|
+
* 2. `<!--` — the script-data tokenizer enters a "script data escaped"
|
|
27
|
+
* state on `<!--`, in which a nested `<script>...</script>` pair can
|
|
28
|
+
* terminate the outer element ahead of the literal closing tag we control.
|
|
29
|
+
* Encoding `<` as the Unicode escape `<` is unambiguous JSON
|
|
30
|
+
* (parses back to `<`) and prevents the tokenizer from ever entering the
|
|
31
|
+
* escaped state.
|
|
32
|
+
*/
|
|
33
|
+
export declare function escapeJsonForScriptTag(json: string): string;
|
|
34
|
+
export declare class HtmlFormatter implements OutputFormatter {
|
|
35
|
+
readonly id = "html";
|
|
36
|
+
readonly fileExtension = ".html";
|
|
37
|
+
format(results: ScanResults): string;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=html-formatter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-formatter.d.ts","sourceRoot":"","sources":["../../src/formatters/html-formatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACV,WAAW,EAKZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,gDAAgD;AAChD,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CASjD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI3D;AA0TD,qBAAa,aAAc,YAAW,eAAe;IACnD,QAAQ,CAAC,EAAE,UAAU;IACrB,QAAQ,CAAC,aAAa,WAAW;IAEjC,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM;CAsCrC"}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML output formatter — self-contained interactive report.
|
|
3
|
+
*
|
|
4
|
+
* Emits a single .html file with inline CSS + JS. Includes:
|
|
5
|
+
* - Header with title, scan timestamp, repository info
|
|
6
|
+
* - Summary statistics (totals + per-status counters)
|
|
7
|
+
* - Filterable, severity-badged issues table
|
|
8
|
+
* - Expandable per-check sections with optional code snippets
|
|
9
|
+
* - The full `ScanResults` embedded as a JSON island for client-side filtering
|
|
10
|
+
*
|
|
11
|
+
* All user-controlled strings are HTML-escaped via `escapeHtml`. The embedded
|
|
12
|
+
* JSON has `</` sequences neutralised so an attacker-controlled string cannot
|
|
13
|
+
* break out of the `<script type="application/json">` tag.
|
|
14
|
+
*/
|
|
15
|
+
/** Escapes the five HTML special characters. */
|
|
16
|
+
export function escapeHtml(value) {
|
|
17
|
+
if (value === undefined || value === null)
|
|
18
|
+
return '';
|
|
19
|
+
const str = String(value);
|
|
20
|
+
return str
|
|
21
|
+
.replace(/&/g, '&')
|
|
22
|
+
.replace(/</g, '<')
|
|
23
|
+
.replace(/>/g, '>')
|
|
24
|
+
.replace(/"/g, '"')
|
|
25
|
+
.replace(/'/g, ''');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Embeds JSON inside a `<script type="application/json">` block safely.
|
|
29
|
+
*
|
|
30
|
+
* Two patterns matter for HTML's script-data tokenizer:
|
|
31
|
+
* 1. `</` — replaced with `<\/` so a stray `</script>` cannot terminate the
|
|
32
|
+
* script element early. `\/` is valid JSON for `/`, so JSON.parse
|
|
33
|
+
* round-trips faithfully.
|
|
34
|
+
* 2. `<!--` — the script-data tokenizer enters a "script data escaped"
|
|
35
|
+
* state on `<!--`, in which a nested `<script>...</script>` pair can
|
|
36
|
+
* terminate the outer element ahead of the literal closing tag we control.
|
|
37
|
+
* Encoding `<` as the Unicode escape `<` is unambiguous JSON
|
|
38
|
+
* (parses back to `<`) and prevents the tokenizer from ever entering the
|
|
39
|
+
* escaped state.
|
|
40
|
+
*/
|
|
41
|
+
export function escapeJsonForScriptTag(json) {
|
|
42
|
+
return json
|
|
43
|
+
.replace(/<\//g, '<\\/')
|
|
44
|
+
.replace(/<!--/g, '\\u003c!--');
|
|
45
|
+
}
|
|
46
|
+
function severityClass(severity) {
|
|
47
|
+
switch (severity) {
|
|
48
|
+
case 'critical':
|
|
49
|
+
case 'high':
|
|
50
|
+
return 'sev-high';
|
|
51
|
+
case 'medium':
|
|
52
|
+
return 'sev-medium';
|
|
53
|
+
case 'low':
|
|
54
|
+
case 'informational':
|
|
55
|
+
return 'sev-low';
|
|
56
|
+
default:
|
|
57
|
+
return 'sev-unknown';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function statusClass(status) {
|
|
61
|
+
switch (status) {
|
|
62
|
+
case 'PASS':
|
|
63
|
+
return 'status-pass';
|
|
64
|
+
case 'FAIL':
|
|
65
|
+
return 'status-fail';
|
|
66
|
+
case 'FLAG':
|
|
67
|
+
return 'status-flag';
|
|
68
|
+
case 'ERROR':
|
|
69
|
+
return 'status-error';
|
|
70
|
+
default:
|
|
71
|
+
return 'status-unknown';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const CSS = `
|
|
75
|
+
:root {
|
|
76
|
+
color-scheme: light dark;
|
|
77
|
+
--bg: #f6f7f9;
|
|
78
|
+
--fg: #1f2329;
|
|
79
|
+
--muted: #6a7280;
|
|
80
|
+
--card: #ffffff;
|
|
81
|
+
--border: #e2e5ea;
|
|
82
|
+
--pass: #15803d;
|
|
83
|
+
--fail: #b91c1c;
|
|
84
|
+
--flag: #b45309;
|
|
85
|
+
--error: #6b21a8;
|
|
86
|
+
--sev-high: #b91c1c;
|
|
87
|
+
--sev-medium: #b45309;
|
|
88
|
+
--sev-low: #2563eb;
|
|
89
|
+
--sev-unknown: #6a7280;
|
|
90
|
+
}
|
|
91
|
+
@media (prefers-color-scheme: dark) {
|
|
92
|
+
:root {
|
|
93
|
+
--bg: #14171c;
|
|
94
|
+
--fg: #e6e8eb;
|
|
95
|
+
--muted: #9ba1aa;
|
|
96
|
+
--card: #1c2026;
|
|
97
|
+
--border: #2a2f37;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
* { box-sizing: border-box; }
|
|
101
|
+
body {
|
|
102
|
+
margin: 0;
|
|
103
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
104
|
+
background: var(--bg);
|
|
105
|
+
color: var(--fg);
|
|
106
|
+
line-height: 1.5;
|
|
107
|
+
}
|
|
108
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
|
109
|
+
header { margin-bottom: 24px; }
|
|
110
|
+
header h1 { margin: 0 0 4px; font-size: 1.6rem; }
|
|
111
|
+
header .meta { color: var(--muted); font-size: 0.9rem; }
|
|
112
|
+
.muted { color: var(--muted); }
|
|
113
|
+
.card {
|
|
114
|
+
background: var(--card);
|
|
115
|
+
border: 1px solid var(--border);
|
|
116
|
+
border-radius: 8px;
|
|
117
|
+
padding: 16px;
|
|
118
|
+
margin-bottom: 16px;
|
|
119
|
+
}
|
|
120
|
+
.summary-grid {
|
|
121
|
+
display: grid;
|
|
122
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
123
|
+
gap: 12px;
|
|
124
|
+
}
|
|
125
|
+
.stat {
|
|
126
|
+
background: var(--card);
|
|
127
|
+
border: 1px solid var(--border);
|
|
128
|
+
border-radius: 8px;
|
|
129
|
+
padding: 12px;
|
|
130
|
+
text-align: center;
|
|
131
|
+
}
|
|
132
|
+
.stat .num { font-size: 1.6rem; font-weight: 600; }
|
|
133
|
+
.stat .label { color: var(--muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
134
|
+
.badge {
|
|
135
|
+
display: inline-block;
|
|
136
|
+
padding: 2px 8px;
|
|
137
|
+
border-radius: 999px;
|
|
138
|
+
font-size: 0.75rem;
|
|
139
|
+
font-weight: 600;
|
|
140
|
+
text-transform: uppercase;
|
|
141
|
+
letter-spacing: 0.04em;
|
|
142
|
+
color: #fff;
|
|
143
|
+
}
|
|
144
|
+
.badge.sev-high { background: var(--sev-high); }
|
|
145
|
+
.badge.sev-medium { background: var(--sev-medium); }
|
|
146
|
+
.badge.sev-low { background: var(--sev-low); }
|
|
147
|
+
.badge.sev-unknown { background: var(--sev-unknown); }
|
|
148
|
+
.badge.status-pass { background: var(--pass); }
|
|
149
|
+
.badge.status-fail { background: var(--fail); }
|
|
150
|
+
.badge.status-flag { background: var(--flag); }
|
|
151
|
+
.badge.status-error { background: var(--error); }
|
|
152
|
+
.badge.status-unknown { background: var(--sev-unknown); }
|
|
153
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
|
|
154
|
+
.controls input, .controls select {
|
|
155
|
+
background: var(--card);
|
|
156
|
+
color: var(--fg);
|
|
157
|
+
border: 1px solid var(--border);
|
|
158
|
+
border-radius: 6px;
|
|
159
|
+
padding: 6px 10px;
|
|
160
|
+
font: inherit;
|
|
161
|
+
}
|
|
162
|
+
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
|
163
|
+
th, td {
|
|
164
|
+
text-align: left;
|
|
165
|
+
padding: 8px 10px;
|
|
166
|
+
border-bottom: 1px solid var(--border);
|
|
167
|
+
vertical-align: top;
|
|
168
|
+
}
|
|
169
|
+
th { background: var(--card); font-weight: 600; }
|
|
170
|
+
tr.hidden { display: none; }
|
|
171
|
+
details { margin-top: 8px; }
|
|
172
|
+
details summary { cursor: pointer; font-weight: 600; }
|
|
173
|
+
pre.snippet {
|
|
174
|
+
background: var(--bg);
|
|
175
|
+
border: 1px solid var(--border);
|
|
176
|
+
border-radius: 6px;
|
|
177
|
+
padding: 8px;
|
|
178
|
+
overflow-x: auto;
|
|
179
|
+
font-size: 0.8rem;
|
|
180
|
+
margin: 8px 0 0;
|
|
181
|
+
}
|
|
182
|
+
.empty { color: var(--muted); font-style: italic; }
|
|
183
|
+
footer { color: var(--muted); font-size: 0.8rem; text-align: center; margin: 32px 0 16px; }
|
|
184
|
+
`;
|
|
185
|
+
const CLIENT_JS = `
|
|
186
|
+
(function () {
|
|
187
|
+
var data;
|
|
188
|
+
try {
|
|
189
|
+
data = JSON.parse(document.getElementById('aghast-results').textContent);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
var search = document.getElementById('filter-search');
|
|
194
|
+
var severity = document.getElementById('filter-severity');
|
|
195
|
+
var status = document.getElementById('filter-status');
|
|
196
|
+
var rows = Array.prototype.slice.call(document.querySelectorAll('#issues-table tbody tr'));
|
|
197
|
+
|
|
198
|
+
function applyFilters() {
|
|
199
|
+
var q = (search.value || '').toLowerCase();
|
|
200
|
+
var sev = severity.value;
|
|
201
|
+
var st = status.value;
|
|
202
|
+
rows.forEach(function (row) {
|
|
203
|
+
var text = row.getAttribute('data-search') || '';
|
|
204
|
+
var rowSev = row.getAttribute('data-severity') || '';
|
|
205
|
+
var rowStatus = row.getAttribute('data-status') || '';
|
|
206
|
+
var matches = (!q || text.indexOf(q) !== -1)
|
|
207
|
+
&& (!sev || rowSev === sev)
|
|
208
|
+
&& (!st || rowStatus === st);
|
|
209
|
+
row.classList.toggle('hidden', !matches);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (search) search.addEventListener('input', applyFilters);
|
|
214
|
+
if (severity) severity.addEventListener('change', applyFilters);
|
|
215
|
+
if (status) status.addEventListener('change', applyFilters);
|
|
216
|
+
|
|
217
|
+
// Expose data for debugging / extensibility.
|
|
218
|
+
window.aghastResults = data;
|
|
219
|
+
})();
|
|
220
|
+
`;
|
|
221
|
+
function renderHeader(results) {
|
|
222
|
+
const repo = results.repository;
|
|
223
|
+
const repoLabel = repo.remoteUrl
|
|
224
|
+
? `${escapeHtml(repo.remoteUrl)}`
|
|
225
|
+
: escapeHtml(repo.path);
|
|
226
|
+
const branch = repo.branch ? ` <span class="muted">on ${escapeHtml(repo.branch)}</span>` : '';
|
|
227
|
+
const commit = repo.commit ? ` <span class="muted">@ ${escapeHtml(repo.commit.slice(0, 12))}</span>` : '';
|
|
228
|
+
return `
|
|
229
|
+
<header>
|
|
230
|
+
<h1>aghast Security Scan Report</h1>
|
|
231
|
+
<div class="meta">
|
|
232
|
+
Scan ID: <code>${escapeHtml(results.scanId)}</code> ·
|
|
233
|
+
Generated: ${escapeHtml(results.timestamp)} ·
|
|
234
|
+
Provider: ${escapeHtml(results.agentProvider.name)}
|
|
235
|
+
</div>
|
|
236
|
+
<div class="meta">
|
|
237
|
+
Repository: ${repoLabel}${branch}${commit}
|
|
238
|
+
</div>
|
|
239
|
+
</header>
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
function renderSummary(summary, _repo) {
|
|
243
|
+
return `
|
|
244
|
+
<section class="summary-grid">
|
|
245
|
+
<div class="stat"><div class="num">${summary.totalChecks}</div><div class="label">Checks</div></div>
|
|
246
|
+
<div class="stat"><div class="num">${summary.passedChecks}</div><div class="label">Passed</div></div>
|
|
247
|
+
<div class="stat"><div class="num">${summary.failedChecks}</div><div class="label">Failed</div></div>
|
|
248
|
+
<div class="stat"><div class="num">${summary.flaggedChecks}</div><div class="label">Flagged</div></div>
|
|
249
|
+
<div class="stat"><div class="num">${summary.errorChecks}</div><div class="label">Errors</div></div>
|
|
250
|
+
<div class="stat"><div class="num">${summary.totalIssues}</div><div class="label">Issues</div></div>
|
|
251
|
+
</section>
|
|
252
|
+
`;
|
|
253
|
+
}
|
|
254
|
+
function renderIssuesTable(issues, statusByCheck) {
|
|
255
|
+
if (issues.length === 0) {
|
|
256
|
+
return `<section class="card"><h2>Issues</h2><p class="empty">No issues detected.</p></section>`;
|
|
257
|
+
}
|
|
258
|
+
const rows = issues.map((issue) => {
|
|
259
|
+
const sev = issue.severity ?? '';
|
|
260
|
+
const sevCls = severityClass(issue.severity);
|
|
261
|
+
const sevBadge = `<span class="badge ${sevCls}">${escapeHtml(sev || 'unknown')}</span>`;
|
|
262
|
+
// Fall back to 'UNKNOWN' (not 'FAIL') for orphaned issues so the row's
|
|
263
|
+
// status badge and the data-status filter don't lie when an issue
|
|
264
|
+
// references a checkId that isn't in the checks list.
|
|
265
|
+
const status = statusByCheck.get(issue.checkId) ?? 'UNKNOWN';
|
|
266
|
+
const statusCls = statusClass(status);
|
|
267
|
+
const statusBadge = `<span class="badge ${statusCls}">${escapeHtml(status)}</span>`;
|
|
268
|
+
const searchBlob = `${issue.checkId} ${issue.checkName} ${issue.file} ${issue.description}`.toLowerCase();
|
|
269
|
+
return `
|
|
270
|
+
<tr data-severity="${escapeHtml(sev)}" data-status="${escapeHtml(status)}" data-search="${escapeHtml(searchBlob)}">
|
|
271
|
+
<td>${escapeHtml(issue.checkId)}</td>
|
|
272
|
+
<td>${escapeHtml(issue.checkName)}</td>
|
|
273
|
+
<td>${statusBadge}</td>
|
|
274
|
+
<td>${sevBadge}</td>
|
|
275
|
+
<td><code>${escapeHtml(issue.file)}</code>:${escapeHtml(issue.startLine)}-${escapeHtml(issue.endLine)}</td>
|
|
276
|
+
<td>${escapeHtml(issue.description)}</td>
|
|
277
|
+
</tr>
|
|
278
|
+
`;
|
|
279
|
+
}).join('');
|
|
280
|
+
return `
|
|
281
|
+
<section class="card">
|
|
282
|
+
<h2>Issues</h2>
|
|
283
|
+
<div class="controls">
|
|
284
|
+
<input id="filter-search" type="search" placeholder="Filter by text..." />
|
|
285
|
+
<select id="filter-severity">
|
|
286
|
+
<option value="">All severities</option>
|
|
287
|
+
<option value="critical">Critical</option>
|
|
288
|
+
<option value="high">High</option>
|
|
289
|
+
<option value="medium">Medium</option>
|
|
290
|
+
<option value="low">Low</option>
|
|
291
|
+
<option value="informational">Informational</option>
|
|
292
|
+
</select>
|
|
293
|
+
<select id="filter-status">
|
|
294
|
+
<option value="">All statuses</option>
|
|
295
|
+
<option value="PASS">Pass</option>
|
|
296
|
+
<option value="FAIL">Fail</option>
|
|
297
|
+
<option value="FLAG">Flag</option>
|
|
298
|
+
<option value="ERROR">Error</option>
|
|
299
|
+
</select>
|
|
300
|
+
</div>
|
|
301
|
+
<table id="issues-table" aria-label="Security issues">
|
|
302
|
+
<thead>
|
|
303
|
+
<tr><th>Check ID</th><th>Check</th><th>Status</th><th>Severity</th><th>Location</th><th>Description</th></tr>
|
|
304
|
+
</thead>
|
|
305
|
+
<tbody>${rows}</tbody>
|
|
306
|
+
</table>
|
|
307
|
+
</section>
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
function renderCheckDetails(checks, issuesByCheck) {
|
|
311
|
+
if (checks.length === 0) {
|
|
312
|
+
return '';
|
|
313
|
+
}
|
|
314
|
+
const sections = checks.map((check) => {
|
|
315
|
+
const checkIssues = issuesByCheck.get(check.checkId) ?? [];
|
|
316
|
+
const statusBadge = `<span class="badge ${statusClass(check.status)}">${escapeHtml(check.status)}</span>`;
|
|
317
|
+
const errorBlock = check.error
|
|
318
|
+
? `<p><strong>Error:</strong> ${escapeHtml(check.error)}</p>`
|
|
319
|
+
: '';
|
|
320
|
+
const issuesBlock = checkIssues.length === 0
|
|
321
|
+
? '<p class="empty">No issues for this check.</p>'
|
|
322
|
+
: checkIssues.map((issue) => {
|
|
323
|
+
const snippet = issue.codeSnippet
|
|
324
|
+
? `<pre class="snippet">${escapeHtml(issue.codeSnippet)}</pre>`
|
|
325
|
+
: '';
|
|
326
|
+
const recommendation = issue.recommendation
|
|
327
|
+
? `<p><strong>Recommendation:</strong> ${escapeHtml(issue.recommendation)}</p>`
|
|
328
|
+
: '';
|
|
329
|
+
return `
|
|
330
|
+
<div class="card">
|
|
331
|
+
<div><strong><code>${escapeHtml(issue.file)}</code>:${escapeHtml(issue.startLine)}-${escapeHtml(issue.endLine)}</strong>
|
|
332
|
+
${issue.severity ? `<span class="badge ${severityClass(issue.severity)}">${escapeHtml(issue.severity)}</span>` : ''}
|
|
333
|
+
</div>
|
|
334
|
+
<p>${escapeHtml(issue.description)}</p>
|
|
335
|
+
${recommendation}
|
|
336
|
+
${snippet}
|
|
337
|
+
</div>
|
|
338
|
+
`;
|
|
339
|
+
}).join('');
|
|
340
|
+
return `
|
|
341
|
+
<details>
|
|
342
|
+
<summary>${escapeHtml(check.checkName)} (${escapeHtml(check.checkId)}) ${statusBadge} · ${check.issuesFound} issue(s)</summary>
|
|
343
|
+
${errorBlock}
|
|
344
|
+
${issuesBlock}
|
|
345
|
+
</details>
|
|
346
|
+
`;
|
|
347
|
+
}).join('');
|
|
348
|
+
return `<section class="card"><h2>Checks</h2>${sections}</section>`;
|
|
349
|
+
}
|
|
350
|
+
export class HtmlFormatter {
|
|
351
|
+
id = 'html';
|
|
352
|
+
fileExtension = '.html';
|
|
353
|
+
format(results) {
|
|
354
|
+
const statusByCheck = new Map();
|
|
355
|
+
const issuesByCheck = new Map();
|
|
356
|
+
for (const check of results.checks) {
|
|
357
|
+
statusByCheck.set(check.checkId, check.status);
|
|
358
|
+
issuesByCheck.set(check.checkId, []);
|
|
359
|
+
}
|
|
360
|
+
for (const issue of results.issues) {
|
|
361
|
+
const arr = issuesByCheck.get(issue.checkId);
|
|
362
|
+
if (arr)
|
|
363
|
+
arr.push(issue);
|
|
364
|
+
else
|
|
365
|
+
issuesByCheck.set(issue.checkId, [issue]);
|
|
366
|
+
}
|
|
367
|
+
const embeddedJson = escapeJsonForScriptTag(JSON.stringify(results));
|
|
368
|
+
const title = `aghast scan ${escapeHtml(results.scanId)}`;
|
|
369
|
+
return `<!DOCTYPE html>
|
|
370
|
+
<html lang="en">
|
|
371
|
+
<head>
|
|
372
|
+
<meta charset="utf-8" />
|
|
373
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
374
|
+
<title>${title}</title>
|
|
375
|
+
<style>${CSS}</style>
|
|
376
|
+
</head>
|
|
377
|
+
<body>
|
|
378
|
+
<div class="container">
|
|
379
|
+
${renderHeader(results)}
|
|
380
|
+
${renderSummary(results.summary, results.repository)}
|
|
381
|
+
${renderIssuesTable(results.issues, statusByCheck)}
|
|
382
|
+
${renderCheckDetails(results.checks, issuesByCheck)}
|
|
383
|
+
<footer>Generated by aghast v${escapeHtml(results.version)}</footer>
|
|
384
|
+
</div>
|
|
385
|
+
<script id="aghast-results" type="application/json">${embeddedJson}</script>
|
|
386
|
+
<script>${CLIENT_JS}</script>
|
|
387
|
+
</body>
|
|
388
|
+
</html>
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
//# sourceMappingURL=html-formatter.js.map
|