v11y-check 0.0.0 → 0.7.0
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/LICENSE +7 -0
- package/README.md +148 -1
- package/bin/v11y.js +11 -0
- package/dist/esm/.cache +1 -0
- package/dist/esm/bin/route.d.ts +1 -0
- package/dist/esm/bin/route.js +170 -0
- package/dist/esm/bin/v11y.d.ts +1 -0
- package/dist/esm/bin/v11y.js +12 -0
- package/dist/esm/cli.d.ts +5 -0
- package/dist/esm/cli.js +231 -0
- package/dist/esm/config.d.ts +10 -0
- package/dist/esm/config.js +41 -0
- package/dist/esm/format-actions.d.ts +3 -0
- package/dist/esm/format-actions.js +34 -0
- package/dist/esm/format-check.d.ts +38 -0
- package/dist/esm/format-check.js +134 -0
- package/dist/esm/format-summary.d.ts +8 -0
- package/dist/esm/format-summary.js +12 -0
- package/dist/esm/format-time.d.ts +1 -0
- package/dist/esm/format-time.js +36 -0
- package/dist/esm/format-verbose.d.ts +3 -0
- package/dist/esm/format-verbose.js +144 -0
- package/dist/esm/generate.d.ts +9 -0
- package/dist/esm/generate.js +28 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +20 -0
- package/dist/esm/init/initCommand.d.ts +6 -0
- package/dist/esm/init/initCommand.js +34 -0
- package/dist/esm/init/scaffold.d.ts +13 -0
- package/dist/esm/init/scaffold.js +45 -0
- package/dist/esm/init/templates.d.ts +1 -0
- package/dist/esm/init/templates.js +23 -0
- package/dist/esm/run-audit.d.ts +34 -0
- package/dist/esm/run-audit.js +166 -0
- package/dist/esm/sync.d.ts +20 -0
- package/dist/esm/sync.js +95 -0
- package/dist/esm/tmp.d.ts +1 -0
- package/dist/esm/tmp.js +14 -0
- package/dist/esm/types.d.ts +98 -0
- package/dist/esm/types.js +40 -0
- package/package.json +52 -2
- package/templates/audit.yaml.template +27 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const HINT_ADD = "Run `v11y sync` to add the listed vulnerabilities to the allowlist.";
|
|
2
|
+
const HINT_REMOVE = "Run `v11y sync` to remove stale allowlist entries.";
|
|
3
|
+
const HINT_BOTH = "Run `v11y sync` to add the listed vulnerabilities to the allowlist and remove stale entries.";
|
|
4
|
+
function buildVerboseFlag(scopes) {
|
|
5
|
+
if (scopes.length === 1) {
|
|
6
|
+
return `--${scopes[0]} --verbose`;
|
|
7
|
+
}
|
|
8
|
+
return "--verbose";
|
|
9
|
+
}
|
|
10
|
+
function selectSyncHint(hasUnallowed, hasStale) {
|
|
11
|
+
if (hasUnallowed && hasStale) return HINT_BOTH;
|
|
12
|
+
if (hasUnallowed) return HINT_ADD;
|
|
13
|
+
return HINT_REMOVE;
|
|
14
|
+
}
|
|
15
|
+
function formatActionHints(result, scopes) {
|
|
16
|
+
const hasUnallowed = scopes.some((scope) => result[scope].unallowed.length > 0);
|
|
17
|
+
const hasAllowed = scopes.some((scope) => result[scope].allowed.length > 0);
|
|
18
|
+
const hasStale = scopes.some((scope) => result[scope].stale.length > 0);
|
|
19
|
+
const hasVulns = hasUnallowed || hasAllowed;
|
|
20
|
+
const needsSync = hasUnallowed || hasStale;
|
|
21
|
+
if (!hasVulns && !needsSync) return "";
|
|
22
|
+
const bullets = [];
|
|
23
|
+
if (hasVulns) {
|
|
24
|
+
bullets.push(` \u2022 Run \`v11y check ${buildVerboseFlag(scopes)}\` for full report`);
|
|
25
|
+
}
|
|
26
|
+
if (needsSync) {
|
|
27
|
+
bullets.push(` \u2022 ${selectSyncHint(hasUnallowed, hasStale)}`);
|
|
28
|
+
}
|
|
29
|
+
return ` Actions:
|
|
30
|
+
${bullets.join("\n")}`;
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
formatActionHints
|
|
34
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AuditResult, AuditScope, SeverityThreshold } from './types.ts';
|
|
2
|
+
export interface AllowedVuln {
|
|
3
|
+
addedAt?: string | undefined;
|
|
4
|
+
cvss?: {
|
|
5
|
+
score?: number;
|
|
6
|
+
vectorString?: string;
|
|
7
|
+
} | undefined;
|
|
8
|
+
description?: string | undefined;
|
|
9
|
+
ghsaId?: string | undefined;
|
|
10
|
+
id: string;
|
|
11
|
+
path: string;
|
|
12
|
+
paths: string[];
|
|
13
|
+
reason?: string | undefined;
|
|
14
|
+
severity?: string | undefined;
|
|
15
|
+
title?: string | undefined;
|
|
16
|
+
url: string;
|
|
17
|
+
}
|
|
18
|
+
export interface StaleEntry {
|
|
19
|
+
id: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ScopeCheckResult {
|
|
22
|
+
allowed: AllowedVuln[];
|
|
23
|
+
belowThreshold: AuditResult[];
|
|
24
|
+
stale: StaleEntry[];
|
|
25
|
+
unallowed: AuditResult[];
|
|
26
|
+
}
|
|
27
|
+
export interface CheckResult {
|
|
28
|
+
dev: ScopeCheckResult;
|
|
29
|
+
prod: ScopeCheckResult;
|
|
30
|
+
}
|
|
31
|
+
export declare function severityIndicator(severity: string | undefined): string;
|
|
32
|
+
export declare function displayId(vuln: {
|
|
33
|
+
ghsaId?: string | undefined;
|
|
34
|
+
id: string;
|
|
35
|
+
}): string;
|
|
36
|
+
export declare function formatSeveritySuffix(severity: string | undefined): string;
|
|
37
|
+
export declare function formatCheckText(result: CheckResult, scopes: AuditScope[], now?: Date, thresholds?: Partial<Record<AuditScope, SeverityThreshold>>): string;
|
|
38
|
+
export declare function formatCheckJson(result: CheckResult, scopes: AuditScope[]): string;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { formatActionHints } from "./format-actions.js";
|
|
2
|
+
import { deriveSummary } from "./format-summary.js";
|
|
3
|
+
import { formatRelativeTime } from "./format-time.js";
|
|
4
|
+
const SEVERITY_INDICATORS = {
|
|
5
|
+
critical: "\u{1F534}",
|
|
6
|
+
high: "\u{1F534}",
|
|
7
|
+
info: "\u{1F7E1}",
|
|
8
|
+
low: "\u{1F7E1}",
|
|
9
|
+
moderate: "\u{1F7E0}"
|
|
10
|
+
};
|
|
11
|
+
function severityIndicator(severity) {
|
|
12
|
+
if (severity === void 0) return "";
|
|
13
|
+
return SEVERITY_INDICATORS[severity] ?? "";
|
|
14
|
+
}
|
|
15
|
+
function displayId(vuln) {
|
|
16
|
+
return vuln.ghsaId ?? vuln.id;
|
|
17
|
+
}
|
|
18
|
+
const SCOPE_LABELS = {
|
|
19
|
+
dev: " \u{1F527} dev:",
|
|
20
|
+
prod: " \u{1F4E6} prod:"
|
|
21
|
+
};
|
|
22
|
+
const SCOPE_NAMES = {
|
|
23
|
+
dev: "dev",
|
|
24
|
+
prod: "prod"
|
|
25
|
+
};
|
|
26
|
+
function formatThresholdAnnotation(threshold) {
|
|
27
|
+
if (threshold === void 0 || threshold === "low") return "";
|
|
28
|
+
const indicator = severityIndicator(threshold);
|
|
29
|
+
const indicatorPart = indicator.length > 0 ? `${indicator} ` : "";
|
|
30
|
+
return `(threshold: ${indicatorPart}${threshold})`;
|
|
31
|
+
}
|
|
32
|
+
function formatIntroBanner(scopes, thresholds) {
|
|
33
|
+
const first = scopes[0];
|
|
34
|
+
if (scopes.length === 1 && first !== void 0) {
|
|
35
|
+
const annotation = formatThresholdAnnotation(thresholds?.[first]);
|
|
36
|
+
return `\u{1F52C} Auditing ${SCOPE_NAMES[first]} dependencies${annotation && ` ${annotation}`} ...`;
|
|
37
|
+
}
|
|
38
|
+
return "\u{1F52C} Auditing dependencies ...";
|
|
39
|
+
}
|
|
40
|
+
function formatSeveritySuffix(severity) {
|
|
41
|
+
if (severity === void 0 || severity === "") return "";
|
|
42
|
+
const emoji = severityIndicator(severity);
|
|
43
|
+
const emojiPart = emoji.length > 0 ? `${emoji} ` : "";
|
|
44
|
+
return ` ${emojiPart}${severity}`;
|
|
45
|
+
}
|
|
46
|
+
function formatAllowedSuffix(addedAt, now) {
|
|
47
|
+
const relative = formatRelativeTime(addedAt, now);
|
|
48
|
+
if (relative.length === 0) return ` \u2022 \u2705 allowed (${addedAt})`;
|
|
49
|
+
return ` \u2022 \u2705 allowed since ${relative} (${addedAt})`;
|
|
50
|
+
}
|
|
51
|
+
function formatUnallowedLine(vuln) {
|
|
52
|
+
return ` \u2022 \u{1F6A8} ${displayId(vuln)}: ${vuln.path}${formatSeveritySuffix(vuln.severity)}`;
|
|
53
|
+
}
|
|
54
|
+
function formatAllowedLine(vuln, now) {
|
|
55
|
+
const suffix = vuln.addedAt !== void 0 ? formatAllowedSuffix(vuln.addedAt, now) : "";
|
|
56
|
+
return ` \u2022 \u26A0\uFE0F ${displayId(vuln)}: ${vuln.path}${formatSeveritySuffix(vuln.severity)}${suffix}`;
|
|
57
|
+
}
|
|
58
|
+
function formatStaleLine(entry) {
|
|
59
|
+
return ` \u2022 \u{1F5D1}\uFE0F ${entry.id} \u2022 not needed`;
|
|
60
|
+
}
|
|
61
|
+
function formatBelowThresholdLine(vuln) {
|
|
62
|
+
return ` \u2022 \u2139\uFE0F ${displayId(vuln)}: ${vuln.path}${formatSeveritySuffix(vuln.severity)} \u2022 \u{1F6AB} ignored`;
|
|
63
|
+
}
|
|
64
|
+
function hasFindings(result) {
|
|
65
|
+
return result.unallowed.length > 0 || result.allowed.length > 0 || result.stale.length > 0 || result.belowThreshold.length > 0;
|
|
66
|
+
}
|
|
67
|
+
function formatScopeFindings(result, now) {
|
|
68
|
+
const lines = [];
|
|
69
|
+
for (const vuln of result.unallowed) {
|
|
70
|
+
lines.push(formatUnallowedLine(vuln));
|
|
71
|
+
}
|
|
72
|
+
for (const vuln of result.allowed) {
|
|
73
|
+
lines.push(formatAllowedLine(vuln, now));
|
|
74
|
+
}
|
|
75
|
+
for (const entry of result.stale) {
|
|
76
|
+
lines.push(formatStaleLine(entry));
|
|
77
|
+
}
|
|
78
|
+
for (const vuln of result.belowThreshold) {
|
|
79
|
+
lines.push(formatBelowThresholdLine(vuln));
|
|
80
|
+
}
|
|
81
|
+
return lines;
|
|
82
|
+
}
|
|
83
|
+
function formatCheckText(result, scopes, now, thresholds) {
|
|
84
|
+
const effectiveNow = now ?? /* @__PURE__ */ new Date();
|
|
85
|
+
const lines = [formatIntroBanner(scopes, thresholds)];
|
|
86
|
+
const anyFindings = scopes.some((scope) => hasFindings(result[scope]));
|
|
87
|
+
if (!anyFindings) {
|
|
88
|
+
lines.push("No known vulnerabilities found.");
|
|
89
|
+
return lines.join("\n") + "\n";
|
|
90
|
+
}
|
|
91
|
+
const singleScope = scopes.length === 1 ? scopes[0] : void 0;
|
|
92
|
+
if (singleScope !== void 0) {
|
|
93
|
+
const scope = singleScope;
|
|
94
|
+
const scopeResult = result[scope];
|
|
95
|
+
lines.push(...formatScopeFindings(scopeResult, effectiveNow));
|
|
96
|
+
const actions2 = formatActionHints(result, scopes);
|
|
97
|
+
if (actions2.length > 0) {
|
|
98
|
+
lines.push("", ...actions2.split("\n").filter((l) => l.length > 0));
|
|
99
|
+
}
|
|
100
|
+
return lines.join("\n") + "\n";
|
|
101
|
+
}
|
|
102
|
+
for (const scope of scopes) {
|
|
103
|
+
const scopeResult = result[scope];
|
|
104
|
+
const annotation = formatThresholdAnnotation(thresholds?.[scope]);
|
|
105
|
+
const header = `${SCOPE_LABELS[scope]}${annotation && ` ${annotation}`}`;
|
|
106
|
+
lines.push(header);
|
|
107
|
+
if (hasFindings(scopeResult)) {
|
|
108
|
+
lines.push(...formatScopeFindings(scopeResult, effectiveNow));
|
|
109
|
+
} else {
|
|
110
|
+
lines.push(" No known vulnerabilities found.");
|
|
111
|
+
}
|
|
112
|
+
lines.push("");
|
|
113
|
+
}
|
|
114
|
+
const actions = formatActionHints(result, scopes);
|
|
115
|
+
if (actions.length > 0) {
|
|
116
|
+
lines.push(...actions.split("\n").filter((l) => l.length > 0));
|
|
117
|
+
}
|
|
118
|
+
return lines.join("\n") + "\n";
|
|
119
|
+
}
|
|
120
|
+
function formatCheckJson(result, scopes) {
|
|
121
|
+
const output = {};
|
|
122
|
+
for (const scope of scopes) {
|
|
123
|
+
output[scope] = result[scope];
|
|
124
|
+
}
|
|
125
|
+
output.summary = deriveSummary(result, scopes);
|
|
126
|
+
return JSON.stringify(output, null, 2) + "\n";
|
|
127
|
+
}
|
|
128
|
+
export {
|
|
129
|
+
displayId,
|
|
130
|
+
formatCheckJson,
|
|
131
|
+
formatCheckText,
|
|
132
|
+
formatSeveritySuffix,
|
|
133
|
+
severityIndicator
|
|
134
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CheckResult } from './format-check.ts';
|
|
2
|
+
import type { AuditScope } from './types.ts';
|
|
3
|
+
export type CheckSummaryStatus = 'vulnerabilities-found' | 'suppressed-vulnerabilities' | 'stale-overrides' | 'none';
|
|
4
|
+
export interface CheckSummary {
|
|
5
|
+
status: CheckSummaryStatus;
|
|
6
|
+
count: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function deriveSummary(result: CheckResult, scopes: AuditScope[]): CheckSummary;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function deriveSummary(result, scopes) {
|
|
2
|
+
const unallowed = scopes.reduce((total, scope) => total + result[scope].unallowed.length, 0);
|
|
3
|
+
if (unallowed > 0) return { status: "vulnerabilities-found", count: unallowed };
|
|
4
|
+
const allowed = scopes.reduce((total, scope) => total + result[scope].allowed.length, 0);
|
|
5
|
+
if (allowed > 0) return { status: "suppressed-vulnerabilities", count: allowed };
|
|
6
|
+
const stale = scopes.reduce((total, scope) => total + result[scope].stale.length, 0);
|
|
7
|
+
if (stale > 0) return { status: "stale-overrides", count: stale };
|
|
8
|
+
return { status: "none", count: 0 };
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
deriveSummary
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatRelativeTime(fromIso: string, now: Date): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function formatRelativeTime(fromIso, now) {
|
|
2
|
+
const from = parseDateUtc(fromIso);
|
|
3
|
+
if (from === void 0) return "";
|
|
4
|
+
const deltaMs = now.getTime() - from.getTime();
|
|
5
|
+
if (deltaMs < 6e4) return "just now";
|
|
6
|
+
const minutes = Math.floor(deltaMs / 6e4);
|
|
7
|
+
if (minutes < 60) return pluralize(minutes, "minute");
|
|
8
|
+
const hours = Math.floor(minutes / 60);
|
|
9
|
+
if (hours < 24) return pluralize(hours, "hour");
|
|
10
|
+
const days = Math.floor(hours / 24);
|
|
11
|
+
if (days === 1) return "yesterday";
|
|
12
|
+
if (days < 7) return pluralize(days, "day");
|
|
13
|
+
const weeks = Math.floor(days / 7);
|
|
14
|
+
if (weeks < 5) return pluralize(weeks, "week");
|
|
15
|
+
const months = diffMonthsUtc(from, now);
|
|
16
|
+
if (months < 12) return pluralize(months, "month");
|
|
17
|
+
const years = Math.floor(months / 12);
|
|
18
|
+
return pluralize(years, "year");
|
|
19
|
+
}
|
|
20
|
+
function parseDateUtc(iso) {
|
|
21
|
+
const parsed = new Date(iso);
|
|
22
|
+
return Number.isNaN(parsed.getTime()) ? void 0 : parsed;
|
|
23
|
+
}
|
|
24
|
+
function diffMonthsUtc(from, now) {
|
|
25
|
+
let months = (now.getUTCFullYear() - from.getUTCFullYear()) * 12 + (now.getUTCMonth() - from.getUTCMonth());
|
|
26
|
+
if (now.getUTCDate() < from.getUTCDate()) {
|
|
27
|
+
months -= 1;
|
|
28
|
+
}
|
|
29
|
+
return Math.max(0, months);
|
|
30
|
+
}
|
|
31
|
+
function pluralize(count, unit) {
|
|
32
|
+
return `${count} ${unit}${count === 1 ? "" : "s"} ago`;
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
formatRelativeTime
|
|
36
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CheckResult } from './format-check.ts';
|
|
2
|
+
import type { AuditScope, SeverityThreshold } from './types.ts';
|
|
3
|
+
export declare function formatCheckVerboseText(result: CheckResult, scopes: AuditScope[], now?: Date, thresholds?: Partial<Record<AuditScope, SeverityThreshold>>): string;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { formatActionHints } from "./format-actions.js";
|
|
2
|
+
import { displayId, formatSeveritySuffix, severityIndicator } from "./format-check.js";
|
|
3
|
+
import { formatRelativeTime } from "./format-time.js";
|
|
4
|
+
const STATUS_UNALLOWED = "\u{1F6A8}";
|
|
5
|
+
const STATUS_ALLOWED = "\u26A0\uFE0F";
|
|
6
|
+
const STATUS_BELOW_THRESHOLD = "\u2139\uFE0F";
|
|
7
|
+
const STATUS_STALE = "\u{1F5D1}\uFE0F";
|
|
8
|
+
const SCOPE_HEADERS = {
|
|
9
|
+
prod: "-- \u{1F4E6} prod --",
|
|
10
|
+
dev: "-- \u{1F527} dev --"
|
|
11
|
+
};
|
|
12
|
+
const DETAIL_INDENT = " ";
|
|
13
|
+
const WRAP_COLUMNS = 72;
|
|
14
|
+
function formatCheckVerboseText(result, scopes, now, thresholds) {
|
|
15
|
+
const effectiveNow = now ?? /* @__PURE__ */ new Date();
|
|
16
|
+
const sections = [];
|
|
17
|
+
for (const scope of scopes) {
|
|
18
|
+
sections.push(formatScopeVerbose(scope, result[scope], effectiveNow, thresholds?.[scope]));
|
|
19
|
+
}
|
|
20
|
+
const actions = formatActionHints(result, scopes);
|
|
21
|
+
const body = sections.join("\n\n") + "\n";
|
|
22
|
+
if (actions.length === 0) return body;
|
|
23
|
+
return body + "\n" + actions + "\n";
|
|
24
|
+
}
|
|
25
|
+
function formatThresholdSuffix(threshold) {
|
|
26
|
+
if (threshold === void 0 || threshold === "low") return "";
|
|
27
|
+
const indicator = severityIndicator(threshold);
|
|
28
|
+
const indicatorPart = indicator.length > 0 ? `${indicator} ` : "";
|
|
29
|
+
return ` (threshold: ${indicatorPart}${threshold})`;
|
|
30
|
+
}
|
|
31
|
+
function formatScopeVerbose(scope, result, now, threshold) {
|
|
32
|
+
const thresholdSuffix = formatThresholdSuffix(threshold);
|
|
33
|
+
const lines = [`${SCOPE_HEADERS[scope]}${thresholdSuffix}`];
|
|
34
|
+
const hasFindings = result.unallowed.length > 0 || result.allowed.length > 0 || result.stale.length > 0 || result.belowThreshold.length > 0;
|
|
35
|
+
if (!hasFindings) {
|
|
36
|
+
lines.push(" (none)");
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
const blocks = [];
|
|
40
|
+
for (const vuln of result.unallowed) {
|
|
41
|
+
blocks.push(formatUnallowedBlock(vuln));
|
|
42
|
+
}
|
|
43
|
+
for (const vuln of result.allowed) {
|
|
44
|
+
blocks.push(formatAllowedBlock(vuln, now));
|
|
45
|
+
}
|
|
46
|
+
for (const entry of result.stale) {
|
|
47
|
+
blocks.push(formatStaleLine(entry));
|
|
48
|
+
}
|
|
49
|
+
for (const vuln of result.belowThreshold) {
|
|
50
|
+
blocks.push(formatBelowThresholdBlock(vuln));
|
|
51
|
+
}
|
|
52
|
+
return lines.concat(blocks.join("\n\n")).join("\n");
|
|
53
|
+
}
|
|
54
|
+
function formatUnallowedBlock(vuln) {
|
|
55
|
+
const headerLine = ` ${STATUS_UNALLOWED} ${displayId(vuln)}${formatSeveritySuffix(vuln.severity)}`;
|
|
56
|
+
const detail = formatAdvisoryDetail(vuln);
|
|
57
|
+
return [headerLine, ...detail].join("\n");
|
|
58
|
+
}
|
|
59
|
+
function formatAllowedBlock(vuln, now) {
|
|
60
|
+
const allowedSuffix = vuln.addedAt !== void 0 ? formatAllowedSuffix(vuln.addedAt, now) : "";
|
|
61
|
+
const headerLine = ` ${STATUS_ALLOWED} ${displayId(vuln)}${formatSeveritySuffix(vuln.severity)}${allowedSuffix}`;
|
|
62
|
+
const hasTitle = vuln.title !== void 0;
|
|
63
|
+
const detail = formatAdvisoryDetail(vuln);
|
|
64
|
+
if (vuln.reason !== void 0) {
|
|
65
|
+
const reasonLineIndex = findReasonInsertionIndex(hasTitle);
|
|
66
|
+
detail.splice(reasonLineIndex, 0, `${DETAIL_INDENT}reason: ${vuln.reason}`);
|
|
67
|
+
}
|
|
68
|
+
return [headerLine, ...detail].join("\n");
|
|
69
|
+
}
|
|
70
|
+
function formatStaleLine(entry) {
|
|
71
|
+
return ` ${STATUS_STALE} ${entry.id} not needed`;
|
|
72
|
+
}
|
|
73
|
+
function formatBelowThresholdBlock(vuln) {
|
|
74
|
+
const headerLine = ` ${STATUS_BELOW_THRESHOLD} ${displayId(vuln)}${formatSeveritySuffix(vuln.severity)} ignored (below threshold)`;
|
|
75
|
+
const detail = formatAdvisoryDetail(vuln);
|
|
76
|
+
return [headerLine, ...detail].join("\n");
|
|
77
|
+
}
|
|
78
|
+
function formatAdvisoryDetail(vuln) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
if (vuln.title !== void 0) {
|
|
81
|
+
lines.push(`${DETAIL_INDENT}${vuln.title}`);
|
|
82
|
+
}
|
|
83
|
+
lines.push(...formatPathsLines(vuln.paths), `${DETAIL_INDENT}link: ${vuln.url}`);
|
|
84
|
+
if (vuln.description !== void 0) {
|
|
85
|
+
lines.push("", ...formatDescriptionLines(vuln.description));
|
|
86
|
+
}
|
|
87
|
+
return lines;
|
|
88
|
+
}
|
|
89
|
+
function findReasonInsertionIndex(hasTitle) {
|
|
90
|
+
return hasTitle ? 1 : 0;
|
|
91
|
+
}
|
|
92
|
+
function formatPathsLines(paths) {
|
|
93
|
+
if (paths.length === 0) return [];
|
|
94
|
+
if (paths.length === 1) {
|
|
95
|
+
return [`${DETAIL_INDENT}path: ${paths[0]}`];
|
|
96
|
+
}
|
|
97
|
+
const lines = [`${DETAIL_INDENT}paths:`];
|
|
98
|
+
for (const pathValue of paths) {
|
|
99
|
+
lines.push(`${DETAIL_INDENT} - ${pathValue}`);
|
|
100
|
+
}
|
|
101
|
+
return lines;
|
|
102
|
+
}
|
|
103
|
+
function formatAllowedSuffix(addedAt, now) {
|
|
104
|
+
const relative = formatRelativeTime(addedAt, now);
|
|
105
|
+
if (relative.length === 0) return ` allowed (${addedAt})`;
|
|
106
|
+
return ` allowed ${relative} (${addedAt})`;
|
|
107
|
+
}
|
|
108
|
+
function formatDescriptionLines(description) {
|
|
109
|
+
const paragraphs = description.split(/\n\s*\n/);
|
|
110
|
+
const lines = [];
|
|
111
|
+
let needsSeparator = false;
|
|
112
|
+
for (const paragraph of paragraphs) {
|
|
113
|
+
if (needsSeparator) lines.push("");
|
|
114
|
+
const wrapped = wrapParagraph(paragraph.trim(), WRAP_COLUMNS);
|
|
115
|
+
for (const wrappedLine of wrapped) {
|
|
116
|
+
lines.push(`${DETAIL_INDENT}${wrappedLine}`);
|
|
117
|
+
}
|
|
118
|
+
needsSeparator = true;
|
|
119
|
+
}
|
|
120
|
+
return lines;
|
|
121
|
+
}
|
|
122
|
+
function wrapParagraph(paragraph, columns) {
|
|
123
|
+
if (paragraph.length === 0) return [];
|
|
124
|
+
const words = paragraph.split(/\s+/);
|
|
125
|
+
const lines = [];
|
|
126
|
+
let current = "";
|
|
127
|
+
for (const word of words) {
|
|
128
|
+
if (current === "") {
|
|
129
|
+
current = word;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (current.length + 1 + word.length > columns) {
|
|
133
|
+
lines.push(current);
|
|
134
|
+
current = word;
|
|
135
|
+
} else {
|
|
136
|
+
current = `${current} ${word}`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (current !== "") lines.push(current);
|
|
140
|
+
return lines;
|
|
141
|
+
}
|
|
142
|
+
export {
|
|
143
|
+
formatCheckVerboseText
|
|
144
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AuditScope, ScopeConfig } from './types.ts';
|
|
2
|
+
interface AuditCiFlatConfig {
|
|
3
|
+
allowlist: string[];
|
|
4
|
+
'show-not-found'?: boolean;
|
|
5
|
+
[key: string]: boolean | string | string[] | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare function buildFlatConfig(scopeConfig: ScopeConfig, scope: AuditScope): AuditCiFlatConfig;
|
|
8
|
+
export declare function generateAuditCiConfig(scopeConfig: ScopeConfig, scope: AuditScope, outputDir: string): Promise<string>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function buildFlatConfig(scopeConfig, scope) {
|
|
4
|
+
const flat = {
|
|
5
|
+
allowlist: scopeConfig.allowlist.map((entry) => entry.id),
|
|
6
|
+
"show-not-found": true
|
|
7
|
+
};
|
|
8
|
+
if (scopeConfig.severityThreshold !== void 0) {
|
|
9
|
+
flat[scopeConfig.severityThreshold] = true;
|
|
10
|
+
}
|
|
11
|
+
if (scope === "prod") {
|
|
12
|
+
flat["skip-dev"] = true;
|
|
13
|
+
} else {
|
|
14
|
+
flat["extra-args"] = ["--dev"];
|
|
15
|
+
}
|
|
16
|
+
return flat;
|
|
17
|
+
}
|
|
18
|
+
async function generateAuditCiConfig(scopeConfig, scope, outputDir) {
|
|
19
|
+
const outputPath = path.join(outputDir, `audit-ci.${scope}.json`);
|
|
20
|
+
const flat = buildFlatConfig(scopeConfig, scope);
|
|
21
|
+
const content = JSON.stringify(flat, null, 2) + "\n";
|
|
22
|
+
await writeFile(outputPath, content, "utf8");
|
|
23
|
+
return outputPath;
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
buildFlatConfig,
|
|
27
|
+
generateAuditCiConfig
|
|
28
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { AllowlistEntry, AuditResult, AuditScope, CommandOptions, ScopeConfig, SeverityThreshold, V11yCheckConfig, } from './types.ts';
|
|
2
|
+
export { allowlistEntrySchema, AUDIT_SCOPES, DEFAULT_CONFIG, scopeConfigSchema, SEVERITY_THRESHOLDS, v11yCheckConfigSchema, } from './types.ts';
|
|
3
|
+
export type { ConfigSource, LoadConfigResult } from './config.ts';
|
|
4
|
+
export { loadConfig } from './config.ts';
|
|
5
|
+
export { syncAllowlist } from './sync.ts';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allowlistEntrySchema,
|
|
3
|
+
AUDIT_SCOPES,
|
|
4
|
+
DEFAULT_CONFIG,
|
|
5
|
+
scopeConfigSchema,
|
|
6
|
+
SEVERITY_THRESHOLDS,
|
|
7
|
+
v11yCheckConfigSchema
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
import { loadConfig } from "./config.js";
|
|
10
|
+
import { syncAllowlist } from "./sync.js";
|
|
11
|
+
export {
|
|
12
|
+
AUDIT_SCOPES,
|
|
13
|
+
DEFAULT_CONFIG,
|
|
14
|
+
SEVERITY_THRESHOLDS,
|
|
15
|
+
allowlistEntrySchema,
|
|
16
|
+
loadConfig,
|
|
17
|
+
scopeConfigSchema,
|
|
18
|
+
syncAllowlist,
|
|
19
|
+
v11yCheckConfigSchema
|
|
20
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { printError, printStep, reportWriteResult } from "@williamthorsen/nmr-core";
|
|
2
|
+
import { extractMessage } from "../cli.js";
|
|
3
|
+
import { scaffoldFiles } from "./scaffold.js";
|
|
4
|
+
function initCommand({ dryRun, force }) {
|
|
5
|
+
if (dryRun) {
|
|
6
|
+
console.info("[dry-run mode]");
|
|
7
|
+
}
|
|
8
|
+
printStep("Scaffolding files");
|
|
9
|
+
let results;
|
|
10
|
+
try {
|
|
11
|
+
results = scaffoldFiles({ dryRun, force });
|
|
12
|
+
} catch (error) {
|
|
13
|
+
printError(`Failed to scaffold files: ${extractMessage(error)}`);
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
for (const result of results) {
|
|
17
|
+
reportWriteResult(result, dryRun);
|
|
18
|
+
}
|
|
19
|
+
if (results.some((r) => r.outcome === "failed")) {
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
if (!dryRun) {
|
|
23
|
+
printStep("Next steps");
|
|
24
|
+
console.info(`
|
|
25
|
+
1. Customize .config/v11y-check.config.json with your severity thresholds.
|
|
26
|
+
2. Commit the scaffolded .github/workflows/audit.yaml alongside your config.
|
|
27
|
+
3. Run: v11y check
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
initCommand
|
|
34
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WriteResult } from '@williamthorsen/nmr-core';
|
|
2
|
+
interface ScaffoldOptions {
|
|
3
|
+
dryRun: boolean;
|
|
4
|
+
force: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface ScaffoldResult {
|
|
7
|
+
configResult: WriteResult;
|
|
8
|
+
}
|
|
9
|
+
export declare function scaffoldConfig({ dryRun, force }: ScaffoldOptions): ScaffoldResult;
|
|
10
|
+
export declare function copyWorkflowTemplate(dryRun: boolean, overwrite: boolean): WriteResult;
|
|
11
|
+
export declare function scaffoldWorkflow(dryRun: boolean, overwrite: boolean): WriteResult;
|
|
12
|
+
export declare function scaffoldFiles({ dryRun, force }: ScaffoldOptions): WriteResult[];
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { findPackageRoot, writeFileWithCheck } from "@williamthorsen/nmr-core";
|
|
4
|
+
import { v11yCheckConfigTemplate } from "./templates.js";
|
|
5
|
+
const CONFIG_PATH = ".config/v11y-check.config.json";
|
|
6
|
+
const WORKFLOW_PATH = ".github/workflows/audit.yaml";
|
|
7
|
+
function scaffoldConfig({ dryRun, force }) {
|
|
8
|
+
const configResult = writeFileWithCheck(CONFIG_PATH, v11yCheckConfigTemplate, { dryRun, overwrite: force });
|
|
9
|
+
return { configResult };
|
|
10
|
+
}
|
|
11
|
+
function copyWorkflowTemplate(dryRun, overwrite) {
|
|
12
|
+
let root;
|
|
13
|
+
try {
|
|
14
|
+
root = findPackageRoot(import.meta.url);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
+
return { filePath: WORKFLOW_PATH, outcome: "failed", error: `Failed to resolve package root: ${message}` };
|
|
18
|
+
}
|
|
19
|
+
const templatePath = resolve(root, "templates", "audit.yaml.template");
|
|
20
|
+
if (!existsSync(templatePath)) {
|
|
21
|
+
return { filePath: WORKFLOW_PATH, outcome: "failed", error: `Could not find bundled template at ${templatePath}` };
|
|
22
|
+
}
|
|
23
|
+
let content;
|
|
24
|
+
try {
|
|
25
|
+
content = readFileSync(templatePath, "utf8");
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
+
return { filePath: WORKFLOW_PATH, outcome: "failed", error: `Failed to read template ${templatePath}: ${message}` };
|
|
29
|
+
}
|
|
30
|
+
return writeFileWithCheck(WORKFLOW_PATH, content, { dryRun, overwrite });
|
|
31
|
+
}
|
|
32
|
+
function scaffoldWorkflow(dryRun, overwrite) {
|
|
33
|
+
return copyWorkflowTemplate(dryRun, overwrite);
|
|
34
|
+
}
|
|
35
|
+
function scaffoldFiles({ dryRun, force }) {
|
|
36
|
+
const { configResult } = scaffoldConfig({ dryRun, force });
|
|
37
|
+
const workflowResult = scaffoldWorkflow(dryRun, force);
|
|
38
|
+
return [configResult, workflowResult];
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
copyWorkflowTemplate,
|
|
42
|
+
scaffoldConfig,
|
|
43
|
+
scaffoldFiles,
|
|
44
|
+
scaffoldWorkflow
|
|
45
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const v11yCheckConfigTemplate: string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readPackageVersion } from "@williamthorsen/nmr-core";
|
|
2
|
+
const VERSION = readPackageVersion(import.meta.url);
|
|
3
|
+
function buildSchemaUrl() {
|
|
4
|
+
return `https://github.com/williamthorsen/node-monorepo-tools/raw/v11y-check-v${VERSION}/packages/v11y-check/schemas/config.json`;
|
|
5
|
+
}
|
|
6
|
+
const v11yCheckConfigTemplate = JSON.stringify(
|
|
7
|
+
{
|
|
8
|
+
$schema: buildSchemaUrl(),
|
|
9
|
+
dev: {
|
|
10
|
+
severityThreshold: "moderate",
|
|
11
|
+
allowlist: []
|
|
12
|
+
},
|
|
13
|
+
prod: {
|
|
14
|
+
severityThreshold: "low",
|
|
15
|
+
allowlist: []
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
null,
|
|
19
|
+
2
|
|
20
|
+
) + "\n";
|
|
21
|
+
export {
|
|
22
|
+
v11yCheckConfigTemplate
|
|
23
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AuditResult } from './types.ts';
|
|
2
|
+
export declare function resolveAuditCiBin(): string;
|
|
3
|
+
export interface ParseResult {
|
|
4
|
+
results: AuditResult[];
|
|
5
|
+
warnings: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function parseAuditCiOutput(jsonString: string): ParseResult;
|
|
8
|
+
export interface StaleEntriesResult {
|
|
9
|
+
entries: string[];
|
|
10
|
+
warnings: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function extractStaleEntries(jsonString: string): StaleEntriesResult;
|
|
13
|
+
interface RunAuditOptions {
|
|
14
|
+
configPath: string;
|
|
15
|
+
cwd?: string;
|
|
16
|
+
json?: boolean;
|
|
17
|
+
reportType?: 'full';
|
|
18
|
+
}
|
|
19
|
+
export interface AuditRunResult {
|
|
20
|
+
exitCode: number;
|
|
21
|
+
staleEntries: string[];
|
|
22
|
+
stdout: string;
|
|
23
|
+
stderr: string;
|
|
24
|
+
warnings: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare function runAudit({ configPath, cwd, json }: RunAuditOptions): AuditRunResult;
|
|
27
|
+
export interface ReportResult {
|
|
28
|
+
results: AuditResult[];
|
|
29
|
+
stdout: string;
|
|
30
|
+
stderr: string;
|
|
31
|
+
warnings: string[];
|
|
32
|
+
}
|
|
33
|
+
export declare function runReport({ configPath, cwd, reportType }: Omit<RunAuditOptions, 'json'>): ReportResult;
|
|
34
|
+
export {};
|