@vertaaux/cli 0.2.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/README.md +345 -0
- package/dist/auth/ci-token.d.ts +49 -0
- package/dist/auth/ci-token.d.ts.map +1 -0
- package/dist/auth/ci-token.js +83 -0
- package/dist/auth/device-flow.d.ts +66 -0
- package/dist/auth/device-flow.d.ts.map +1 -0
- package/dist/auth/device-flow.js +156 -0
- package/dist/auth/token-store.d.ts +53 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +78 -0
- package/dist/baseline/diff.d.ts +57 -0
- package/dist/baseline/diff.d.ts.map +1 -0
- package/dist/baseline/diff.js +152 -0
- package/dist/baseline/hash.d.ts +54 -0
- package/dist/baseline/hash.d.ts.map +1 -0
- package/dist/baseline/hash.js +66 -0
- package/dist/baseline/manager.d.ts +89 -0
- package/dist/baseline/manager.d.ts.map +1 -0
- package/dist/baseline/manager.js +157 -0
- package/dist/cache/index.d.ts +8 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +7 -0
- package/dist/cache/route-cache.d.ts +119 -0
- package/dist/cache/route-cache.d.ts.map +1 -0
- package/dist/cache/route-cache.js +213 -0
- package/dist/ci/changed-routes.d.ts +95 -0
- package/dist/ci/changed-routes.d.ts.map +1 -0
- package/dist/ci/changed-routes.js +304 -0
- package/dist/ci/github-api.d.ts +68 -0
- package/dist/ci/github-api.d.ts.map +1 -0
- package/dist/ci/github-api.js +138 -0
- package/dist/ci/gitlab-api.d.ts +75 -0
- package/dist/ci/gitlab-api.d.ts.map +1 -0
- package/dist/ci/gitlab-api.js +180 -0
- package/dist/ci/index.d.ts +6 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +4 -0
- package/dist/commands/audit.d.ts +58 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +862 -0
- package/dist/commands/baseline.d.ts +22 -0
- package/dist/commands/baseline.d.ts.map +1 -0
- package/dist/commands/baseline.js +210 -0
- package/dist/commands/comment.d.ts +14 -0
- package/dist/commands/comment.d.ts.map +1 -0
- package/dist/commands/comment.js +363 -0
- package/dist/commands/diff.d.ts +24 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +196 -0
- package/dist/commands/doctor.d.ts +58 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +338 -0
- package/dist/commands/download.d.ts +12 -0
- package/dist/commands/download.d.ts.map +1 -0
- package/dist/commands/download.js +183 -0
- package/dist/commands/explain.d.ts +62 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +302 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +212 -0
- package/dist/commands/login.d.ts +14 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +222 -0
- package/dist/commands/policy.d.ts +13 -0
- package/dist/commands/policy.d.ts.map +1 -0
- package/dist/commands/policy.js +347 -0
- package/dist/commands/upload.d.ts +12 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +158 -0
- package/dist/config/defaults.d.ts +21 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +49 -0
- package/dist/config/loader.d.ts +66 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +167 -0
- package/dist/config/schema.d.ts +55 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +6 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1090 -0
- package/dist/interactive/fix-wizard.d.ts +44 -0
- package/dist/interactive/fix-wizard.d.ts.map +1 -0
- package/dist/interactive/fix-wizard.js +286 -0
- package/dist/interactive/init-wizard.d.ts +32 -0
- package/dist/interactive/init-wizard.d.ts.map +1 -0
- package/dist/interactive/init-wizard.js +193 -0
- package/dist/interactive/prompts.d.ts +62 -0
- package/dist/interactive/prompts.d.ts.map +1 -0
- package/dist/interactive/prompts.js +78 -0
- package/dist/monorepo/detector.d.ts +70 -0
- package/dist/monorepo/detector.d.ts.map +1 -0
- package/dist/monorepo/detector.js +278 -0
- package/dist/monorepo/index.d.ts +9 -0
- package/dist/monorepo/index.d.ts.map +1 -0
- package/dist/monorepo/index.js +8 -0
- package/dist/monorepo/workspace.d.ts +142 -0
- package/dist/monorepo/workspace.d.ts.map +1 -0
- package/dist/monorepo/workspace.js +171 -0
- package/dist/output/envelope.d.ts +21 -0
- package/dist/output/envelope.d.ts.map +1 -0
- package/dist/output/envelope.js +27 -0
- package/dist/output/factory.d.ts +73 -0
- package/dist/output/factory.d.ts.map +1 -0
- package/dist/output/factory.js +60 -0
- package/dist/output/formats.d.ts +11 -0
- package/dist/output/formats.d.ts.map +1 -0
- package/dist/output/formats.js +41 -0
- package/dist/output/html.d.ts +45 -0
- package/dist/output/html.d.ts.map +1 -0
- package/dist/output/html.js +607 -0
- package/dist/output/human.d.ts +41 -0
- package/dist/output/human.d.ts.map +1 -0
- package/dist/output/human.js +274 -0
- package/dist/output/json.d.ts +42 -0
- package/dist/output/json.d.ts.map +1 -0
- package/dist/output/json.js +37 -0
- package/dist/output/junit.d.ts +56 -0
- package/dist/output/junit.d.ts.map +1 -0
- package/dist/output/junit.js +135 -0
- package/dist/output/markdown.d.ts +77 -0
- package/dist/output/markdown.d.ts.map +1 -0
- package/dist/output/markdown.js +411 -0
- package/dist/output/sarif.d.ts +160 -0
- package/dist/output/sarif.d.ts.map +1 -0
- package/dist/output/sarif.js +207 -0
- package/dist/policy/evaluator.d.ts +111 -0
- package/dist/policy/evaluator.d.ts.map +1 -0
- package/dist/policy/evaluator.js +362 -0
- package/dist/policy/index.d.ts +15 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +11 -0
- package/dist/policy/loader.d.ts +97 -0
- package/dist/policy/loader.d.ts.map +1 -0
- package/dist/policy/loader.js +281 -0
- package/dist/policy/schema.d.ts +297 -0
- package/dist/policy/schema.d.ts.map +1 -0
- package/dist/policy/schema.js +230 -0
- package/dist/quality-gate/evaluator.d.ts +58 -0
- package/dist/quality-gate/evaluator.d.ts.map +1 -0
- package/dist/quality-gate/evaluator.js +274 -0
- package/dist/quality-gate/index.d.ts +10 -0
- package/dist/quality-gate/index.d.ts.map +1 -0
- package/dist/quality-gate/index.js +7 -0
- package/dist/quality-gate/types.d.ts +103 -0
- package/dist/quality-gate/types.d.ts.map +1 -0
- package/dist/quality-gate/types.js +23 -0
- package/dist/templates/azure-devops.d.ts +25 -0
- package/dist/templates/azure-devops.d.ts.map +1 -0
- package/dist/templates/azure-devops.js +109 -0
- package/dist/templates/circleci.d.ts +28 -0
- package/dist/templates/circleci.d.ts.map +1 -0
- package/dist/templates/circleci.js +86 -0
- package/dist/templates/github-actions.d.ts +81 -0
- package/dist/templates/github-actions.d.ts.map +1 -0
- package/dist/templates/github-actions.js +393 -0
- package/dist/templates/gitlab-ci.d.ts +26 -0
- package/dist/templates/gitlab-ci.d.ts.map +1 -0
- package/dist/templates/gitlab-ci.js +70 -0
- package/dist/templates/index.d.ts +72 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +112 -0
- package/dist/templates/jenkins.d.ts +26 -0
- package/dist/templates/jenkins.d.ts.map +1 -0
- package/dist/templates/jenkins.js +110 -0
- package/dist/ui/banner.d.ts +31 -0
- package/dist/ui/banner.d.ts.map +1 -0
- package/dist/ui/banner.js +84 -0
- package/dist/ui/diagnostics.d.ts +39 -0
- package/dist/ui/diagnostics.d.ts.map +1 -0
- package/dist/ui/diagnostics.js +153 -0
- package/dist/ui/spinner.d.ts +61 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +101 -0
- package/dist/ui/table.d.ts +63 -0
- package/dist/ui/table.d.ts.map +1 -0
- package/dist/ui/table.js +236 -0
- package/dist/utils/client.d.ts +82 -0
- package/dist/utils/client.d.ts.map +1 -0
- package/dist/utils/client.js +128 -0
- package/dist/utils/detect-env.d.ts +59 -0
- package/dist/utils/detect-env.d.ts.map +1 -0
- package/dist/utils/detect-env.js +115 -0
- package/dist/utils/exit-codes.d.ts +47 -0
- package/dist/utils/exit-codes.d.ts.map +1 -0
- package/dist/utils/exit-codes.js +61 -0
- package/dist/utils/logger.d.ts +87 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +185 -0
- package/dist/utils/sanitize.d.ts +36 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +64 -0
- package/dist/utils/validators.d.ts +41 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +123 -0
- package/package.json +63 -0
- package/schemas/vertaaux.config.schema.json +103 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown formatter for PR comments.
|
|
3
|
+
*
|
|
4
|
+
* Generates GitHub/GitLab flavored markdown with:
|
|
5
|
+
* - Collapsible sections for grouping issues
|
|
6
|
+
* - Evidence links for each issue
|
|
7
|
+
* - New/fixed issue labeling from baseline comparison
|
|
8
|
+
* - Hidden identifier for sticky comment matching
|
|
9
|
+
*/
|
|
10
|
+
import { generateFingerprint } from "../baseline/hash.js";
|
|
11
|
+
/**
|
|
12
|
+
* Default markdown options.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_MARKDOWN_OPTIONS = {
|
|
15
|
+
groupBy: "severity",
|
|
16
|
+
collapse: true,
|
|
17
|
+
collapseThreshold: 3,
|
|
18
|
+
includeEvidence: true,
|
|
19
|
+
includeFixes: true,
|
|
20
|
+
baseUrl: "https://vertaaux.ai",
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Emoji mappings for severities.
|
|
24
|
+
*/
|
|
25
|
+
const SEVERITY_EMOJI = {
|
|
26
|
+
error: ":red_circle:",
|
|
27
|
+
critical: ":red_circle:",
|
|
28
|
+
warning: ":yellow_circle:",
|
|
29
|
+
serious: ":yellow_circle:",
|
|
30
|
+
info: ":blue_circle:",
|
|
31
|
+
minor: ":blue_circle:",
|
|
32
|
+
moderate: ":large_orange_circle:",
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Status emoji mappings.
|
|
36
|
+
*/
|
|
37
|
+
const STATUS_EMOJI = {
|
|
38
|
+
success: ":white_check_mark:",
|
|
39
|
+
warning: ":warning:",
|
|
40
|
+
error: ":x:",
|
|
41
|
+
new: ":new:",
|
|
42
|
+
fixed: ":white_check_mark:",
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Normalize severity to standard values.
|
|
46
|
+
*/
|
|
47
|
+
function normalizeSeverity(severity) {
|
|
48
|
+
const sev = (severity || "info").toLowerCase();
|
|
49
|
+
if (sev === "critical")
|
|
50
|
+
return "error";
|
|
51
|
+
if (sev === "serious")
|
|
52
|
+
return "warning";
|
|
53
|
+
if (sev === "minor" || sev === "moderate")
|
|
54
|
+
return "info";
|
|
55
|
+
return sev;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get rule ID from issue.
|
|
59
|
+
*/
|
|
60
|
+
function getRuleId(issue) {
|
|
61
|
+
if ("ruleId" in issue && issue.ruleId)
|
|
62
|
+
return issue.ruleId;
|
|
63
|
+
if ("rule_id" in issue && issue.rule_id)
|
|
64
|
+
return issue.rule_id;
|
|
65
|
+
if ("id" in issue && issue.id)
|
|
66
|
+
return issue.id;
|
|
67
|
+
return "unknown";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Count issues by severity.
|
|
71
|
+
*/
|
|
72
|
+
function countIssues(issues) {
|
|
73
|
+
const counts = { error: 0, warning: 0, info: 0, total: 0 };
|
|
74
|
+
for (const issue of issues) {
|
|
75
|
+
const sev = normalizeSeverity(issue.severity);
|
|
76
|
+
if (sev === "error")
|
|
77
|
+
counts.error++;
|
|
78
|
+
else if (sev === "warning")
|
|
79
|
+
counts.warning++;
|
|
80
|
+
else
|
|
81
|
+
counts.info++;
|
|
82
|
+
counts.total++;
|
|
83
|
+
}
|
|
84
|
+
return counts;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Extract grouping key from issue.
|
|
88
|
+
*/
|
|
89
|
+
function getGroupKey(issue, groupBy) {
|
|
90
|
+
switch (groupBy) {
|
|
91
|
+
case "severity":
|
|
92
|
+
return normalizeSeverity(issue.severity);
|
|
93
|
+
case "category":
|
|
94
|
+
return issue.category || "General";
|
|
95
|
+
case "route": {
|
|
96
|
+
// Extract route from selector or URL
|
|
97
|
+
const routeMatch = issue.selector?.match(/\[data-route="([^"]+)"\]/);
|
|
98
|
+
if (routeMatch)
|
|
99
|
+
return routeMatch[1];
|
|
100
|
+
// Fall back to extracting path from description if it contains URL-like paths
|
|
101
|
+
const pathMatch = issue.description?.match(/\/[a-z0-9-/]+/i);
|
|
102
|
+
return pathMatch ? pathMatch[0] : "/";
|
|
103
|
+
}
|
|
104
|
+
case "file": {
|
|
105
|
+
// Try to extract file path from issue
|
|
106
|
+
const fileIssue = issue;
|
|
107
|
+
return fileIssue.file || fileIssue.filePath || "Unknown File";
|
|
108
|
+
}
|
|
109
|
+
case "component": {
|
|
110
|
+
// Extract component name from selector or class
|
|
111
|
+
const classMatch = issue.selector?.match(/\.([A-Z][a-zA-Z0-9]+)/);
|
|
112
|
+
if (classMatch)
|
|
113
|
+
return classMatch[1];
|
|
114
|
+
// Try data-component attribute
|
|
115
|
+
const compMatch = issue.selector?.match(/\[data-component="([^"]+)"\]/);
|
|
116
|
+
if (compMatch)
|
|
117
|
+
return compMatch[1];
|
|
118
|
+
// Try extracting from tag name patterns like Button, Card, etc
|
|
119
|
+
const tagMatch = issue.selector?.match(/(?:^|\s)([A-Z][a-zA-Z0-9]+)(?:\[|\.|\s|$)/);
|
|
120
|
+
return tagMatch ? tagMatch[1] : "General";
|
|
121
|
+
}
|
|
122
|
+
default:
|
|
123
|
+
return "General";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Group issues by the specified field.
|
|
128
|
+
*/
|
|
129
|
+
function groupIssues(issues, groupBy) {
|
|
130
|
+
const groups = new Map();
|
|
131
|
+
for (const issue of issues) {
|
|
132
|
+
const key = getGroupKey(issue, groupBy);
|
|
133
|
+
if (!groups.has(key)) {
|
|
134
|
+
groups.set(key, []);
|
|
135
|
+
}
|
|
136
|
+
groups.get(key).push(issue);
|
|
137
|
+
}
|
|
138
|
+
return groups;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Sort group keys for consistent ordering.
|
|
142
|
+
*/
|
|
143
|
+
function sortGroupKeys(keys, groupBy) {
|
|
144
|
+
if (groupBy === "severity") {
|
|
145
|
+
const priority = { error: 0, warning: 1, info: 2 };
|
|
146
|
+
return keys.sort((a, b) => (priority[a] ?? 3) - (priority[b] ?? 3));
|
|
147
|
+
}
|
|
148
|
+
return keys.sort((a, b) => a.localeCompare(b));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Format severity label for display.
|
|
152
|
+
*/
|
|
153
|
+
function formatSeverityLabel(severity) {
|
|
154
|
+
const normalized = normalizeSeverity(severity);
|
|
155
|
+
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Format a single issue as markdown.
|
|
159
|
+
*/
|
|
160
|
+
function formatIssue(issue, options) {
|
|
161
|
+
const emoji = SEVERITY_EMOJI[normalizeSeverity(issue.severity)] || ":grey_question:";
|
|
162
|
+
const severity = formatSeverityLabel(issue.severity || "info");
|
|
163
|
+
const ruleId = getRuleId(issue);
|
|
164
|
+
const description = issue.description || issue.title || ruleId;
|
|
165
|
+
const fingerprint = generateFingerprint(issue);
|
|
166
|
+
const lines = [];
|
|
167
|
+
// Header with severity and rule
|
|
168
|
+
lines.push(`#### ${emoji} ${severity}: ${ruleId}`);
|
|
169
|
+
lines.push(`**Description:** ${description}`);
|
|
170
|
+
if (issue.selector) {
|
|
171
|
+
lines.push(`**Selector:** \`${issue.selector}\``);
|
|
172
|
+
}
|
|
173
|
+
// Fix suggestion if available and enabled
|
|
174
|
+
if (options.includeFixes) {
|
|
175
|
+
const fix = issue.recommendation || issue.recommended_fix;
|
|
176
|
+
if (fix) {
|
|
177
|
+
lines.push(`**Fix:** ${fix}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// WCAG reference if available
|
|
181
|
+
if (issue.wcag_reference) {
|
|
182
|
+
lines.push(`**WCAG:** ${issue.wcag_reference}`);
|
|
183
|
+
}
|
|
184
|
+
// Evidence link if enabled
|
|
185
|
+
if (options.includeEvidence) {
|
|
186
|
+
lines.push(`[View evidence](${options.baseUrl}/evidence/${fingerprint})`);
|
|
187
|
+
}
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Format fixed issue summary.
|
|
192
|
+
*/
|
|
193
|
+
function formatFixedIssue(issue) {
|
|
194
|
+
const ruleId = issue.ruleId || "unknown";
|
|
195
|
+
const description = issue.description || ruleId;
|
|
196
|
+
return `${STATUS_EMOJI.fixed} ~~${ruleId}~~: ${description}`;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Generate status line based on issue counts.
|
|
200
|
+
*/
|
|
201
|
+
function generateStatusLine(newCount, fixedCount, existingCount) {
|
|
202
|
+
if (newCount === 0 && fixedCount === 0 && existingCount === 0) {
|
|
203
|
+
return `**Status:** :green_circle: All clear! No issues found.`;
|
|
204
|
+
}
|
|
205
|
+
const parts = [];
|
|
206
|
+
if (newCount > 0) {
|
|
207
|
+
parts.push(`${STATUS_EMOJI.warning} ${newCount} new issue${newCount === 1 ? "" : "s"} found`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
parts.push(`:green_circle: No new issues`);
|
|
211
|
+
}
|
|
212
|
+
if (fixedCount > 0) {
|
|
213
|
+
parts.push(`${STATUS_EMOJI.fixed} ${fixedCount} fixed`);
|
|
214
|
+
}
|
|
215
|
+
return `**Status:** ${parts.join(" | ")}`;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Generate summary table.
|
|
219
|
+
*/
|
|
220
|
+
function generateSummaryTable(newIssues, existingIssues, groupBy) {
|
|
221
|
+
const lines = [];
|
|
222
|
+
if (groupBy === "category") {
|
|
223
|
+
// Category-based summary
|
|
224
|
+
const categories = new Set();
|
|
225
|
+
[...newIssues, ...existingIssues].forEach((i) => {
|
|
226
|
+
categories.add(i.category || "General");
|
|
227
|
+
});
|
|
228
|
+
const newByCategory = new Map();
|
|
229
|
+
const existingByCategory = new Map();
|
|
230
|
+
newIssues.forEach((i) => {
|
|
231
|
+
const cat = i.category || "General";
|
|
232
|
+
newByCategory.set(cat, (newByCategory.get(cat) || 0) + 1);
|
|
233
|
+
});
|
|
234
|
+
existingIssues.forEach((i) => {
|
|
235
|
+
const cat = i.category || "General";
|
|
236
|
+
existingByCategory.set(cat, (existingByCategory.get(cat) || 0) + 1);
|
|
237
|
+
});
|
|
238
|
+
lines.push("| Category | New | Existing | Total |");
|
|
239
|
+
lines.push("|----------|-----|----------|-------|");
|
|
240
|
+
for (const cat of Array.from(categories).sort()) {
|
|
241
|
+
const newCount = newByCategory.get(cat) || 0;
|
|
242
|
+
const existingCount = existingByCategory.get(cat) || 0;
|
|
243
|
+
lines.push(`| ${cat} | ${newCount} | ${existingCount} | ${newCount + existingCount} |`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
// Default: severity-based summary
|
|
248
|
+
const newCounts = countIssues(newIssues);
|
|
249
|
+
const existingCounts = countIssues(existingIssues);
|
|
250
|
+
lines.push("| Severity | New | Existing | Total |");
|
|
251
|
+
lines.push("|----------|-----|----------|-------|");
|
|
252
|
+
lines.push(`| :red_circle: Error | ${newCounts.error} | ${existingCounts.error} | ${newCounts.error + existingCounts.error} |`);
|
|
253
|
+
lines.push(`| :yellow_circle: Warning | ${newCounts.warning} | ${existingCounts.warning} | ${newCounts.warning + existingCounts.warning} |`);
|
|
254
|
+
lines.push(`| :blue_circle: Info | ${newCounts.info} | ${existingCounts.info} | ${newCounts.info + existingCounts.info} |`);
|
|
255
|
+
}
|
|
256
|
+
return lines;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Generate scores section.
|
|
260
|
+
*/
|
|
261
|
+
function generateScoresSection(scores) {
|
|
262
|
+
if (!scores)
|
|
263
|
+
return [];
|
|
264
|
+
const parts = [];
|
|
265
|
+
if (scores.overall !== undefined) {
|
|
266
|
+
parts.push(`**Overall:** ${scores.overall}/100`);
|
|
267
|
+
}
|
|
268
|
+
if (scores.accessibility !== undefined) {
|
|
269
|
+
parts.push(`**Accessibility:** ${scores.accessibility}/100`);
|
|
270
|
+
}
|
|
271
|
+
if (parts.length === 0)
|
|
272
|
+
return [];
|
|
273
|
+
return ["### Scores", "", parts.join(" | "), ""];
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Format markdown comment from audit data.
|
|
277
|
+
*
|
|
278
|
+
* Produces GitHub-flavored markdown with:
|
|
279
|
+
* - Hidden identifier for sticky comment matching
|
|
280
|
+
* - Status summary line
|
|
281
|
+
* - Issue counts table
|
|
282
|
+
* - Grouped and optionally collapsible issue sections
|
|
283
|
+
* - Evidence links for each issue
|
|
284
|
+
* - Footer with audit ID links
|
|
285
|
+
*
|
|
286
|
+
* @param data - Comment data with categorized issues
|
|
287
|
+
* @param options - Formatting options
|
|
288
|
+
* @returns Formatted markdown string
|
|
289
|
+
*/
|
|
290
|
+
export function formatMarkdownComment(data, options = {}) {
|
|
291
|
+
const opts = { ...DEFAULT_MARKDOWN_OPTIONS, ...options };
|
|
292
|
+
const { newIssues, fixedIssues, existingIssues, auditId, scores } = data;
|
|
293
|
+
// Determine if we should collapse based on threshold
|
|
294
|
+
const shouldCollapse = opts.collapse && newIssues.length > opts.collapseThreshold;
|
|
295
|
+
const lines = [];
|
|
296
|
+
// Hidden identifier for sticky comment matching (CRITICAL)
|
|
297
|
+
lines.push("<!-- vertaaux-audit -->");
|
|
298
|
+
lines.push("## VertaaUX Audit Results");
|
|
299
|
+
lines.push("");
|
|
300
|
+
// Status line
|
|
301
|
+
lines.push(generateStatusLine(newIssues.length, fixedIssues.length, existingIssues.length));
|
|
302
|
+
lines.push("");
|
|
303
|
+
// Summary table
|
|
304
|
+
if (newIssues.length > 0 || existingIssues.length > 0) {
|
|
305
|
+
lines.push("### Summary");
|
|
306
|
+
lines.push("");
|
|
307
|
+
lines.push(...generateSummaryTable(newIssues, existingIssues, opts.groupBy));
|
|
308
|
+
lines.push("");
|
|
309
|
+
}
|
|
310
|
+
// Scores section
|
|
311
|
+
lines.push(...generateScoresSection(scores));
|
|
312
|
+
// New issues section
|
|
313
|
+
if (newIssues.length > 0) {
|
|
314
|
+
const grouped = groupIssues(newIssues, opts.groupBy);
|
|
315
|
+
const sortedKeys = sortGroupKeys(Array.from(grouped.keys()), opts.groupBy);
|
|
316
|
+
if (shouldCollapse) {
|
|
317
|
+
lines.push("<details>");
|
|
318
|
+
lines.push(`<summary><strong>${STATUS_EMOJI.new} New Issues (${newIssues.length})</strong></summary>`);
|
|
319
|
+
lines.push("");
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
lines.push(`### ${STATUS_EMOJI.new} New Issues (${newIssues.length})`);
|
|
323
|
+
lines.push("");
|
|
324
|
+
}
|
|
325
|
+
for (const groupKey of sortedKeys) {
|
|
326
|
+
const groupedIssues = grouped.get(groupKey);
|
|
327
|
+
// Add group header if grouping by something other than severity with single group
|
|
328
|
+
if (sortedKeys.length > 1 || opts.groupBy !== "severity") {
|
|
329
|
+
const groupLabel = opts.groupBy === "severity"
|
|
330
|
+
? formatSeverityLabel(groupKey)
|
|
331
|
+
: groupKey;
|
|
332
|
+
lines.push(`**${groupLabel}** (${groupedIssues.length})`);
|
|
333
|
+
lines.push("");
|
|
334
|
+
}
|
|
335
|
+
for (const issue of groupedIssues) {
|
|
336
|
+
lines.push(formatIssue(issue, opts));
|
|
337
|
+
lines.push("");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (shouldCollapse) {
|
|
341
|
+
lines.push("</details>");
|
|
342
|
+
lines.push("");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Fixed issues section (always collapsible)
|
|
346
|
+
if (fixedIssues.length > 0) {
|
|
347
|
+
lines.push("<details>");
|
|
348
|
+
lines.push(`<summary><strong>${STATUS_EMOJI.fixed} Fixed Issues (${fixedIssues.length})</strong></summary>`);
|
|
349
|
+
lines.push("");
|
|
350
|
+
for (const issue of fixedIssues) {
|
|
351
|
+
lines.push(formatFixedIssue(issue));
|
|
352
|
+
}
|
|
353
|
+
lines.push("");
|
|
354
|
+
lines.push("</details>");
|
|
355
|
+
lines.push("");
|
|
356
|
+
}
|
|
357
|
+
// Existing issues count (collapsed, just a note)
|
|
358
|
+
if (existingIssues.length > 0) {
|
|
359
|
+
lines.push("<details>");
|
|
360
|
+
lines.push(`<summary><em>Existing Issues (${existingIssues.length} baselined)</em></summary>`);
|
|
361
|
+
lines.push("");
|
|
362
|
+
lines.push("These issues are tracked in baseline and will not fail the build.");
|
|
363
|
+
lines.push("");
|
|
364
|
+
lines.push("</details>");
|
|
365
|
+
lines.push("");
|
|
366
|
+
}
|
|
367
|
+
// Footer
|
|
368
|
+
lines.push("---");
|
|
369
|
+
if (auditId) {
|
|
370
|
+
lines.push(`*Audit ID: [${auditId}](${opts.baseUrl}/audit/${auditId}) | [Full Report](${opts.baseUrl}/report/${auditId})*`);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
const timestamp = new Date().toISOString().split("T")[0];
|
|
374
|
+
lines.push(`*Generated by [VertaaUX](${opts.baseUrl}) on ${timestamp}*`);
|
|
375
|
+
}
|
|
376
|
+
return lines.join("\n");
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Categorize issues into new/existing/fixed based on baseline.
|
|
380
|
+
*
|
|
381
|
+
* @param currentIssues - Issues from current audit
|
|
382
|
+
* @param baseline - Baseline for comparison (null = all issues are new)
|
|
383
|
+
* @returns Categorized issues for comment data
|
|
384
|
+
*/
|
|
385
|
+
export function categorizeIssuesForComment(currentIssues, baseline) {
|
|
386
|
+
if (!baseline) {
|
|
387
|
+
return {
|
|
388
|
+
newIssues: currentIssues,
|
|
389
|
+
fixedIssues: [],
|
|
390
|
+
existingIssues: [],
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const baselineFingerprints = new Set(baseline.issues.map((i) => i.fingerprint));
|
|
394
|
+
const currentFingerprints = new Map();
|
|
395
|
+
for (const issue of currentIssues) {
|
|
396
|
+
currentFingerprints.set(generateFingerprint(issue), issue);
|
|
397
|
+
}
|
|
398
|
+
const newIssues = [];
|
|
399
|
+
const existingIssues = [];
|
|
400
|
+
for (const [fingerprint, issue] of currentFingerprints) {
|
|
401
|
+
if (baselineFingerprints.has(fingerprint)) {
|
|
402
|
+
existingIssues.push(issue);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
newIssues.push(issue);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// Fixed = in baseline but not in current
|
|
409
|
+
const fixedIssues = baseline.issues.filter((bi) => !currentFingerprints.has(bi.fingerprint));
|
|
410
|
+
return { newIssues, fixedIssues, existingIssues };
|
|
411
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SARIF 2.1.0 output formatter for CLI.
|
|
3
|
+
*
|
|
4
|
+
* Generates SARIF (Static Analysis Results Interchange Format) output
|
|
5
|
+
* for integration with GitHub Code Scanning and other SARIF consumers.
|
|
6
|
+
*
|
|
7
|
+
* @see https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
|
|
8
|
+
* @see https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* SARIF log root structure.
|
|
12
|
+
*/
|
|
13
|
+
export interface SarifLog {
|
|
14
|
+
$schema: string;
|
|
15
|
+
version: "2.1.0";
|
|
16
|
+
runs: SarifRun[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* SARIF run containing tool info and results.
|
|
20
|
+
*/
|
|
21
|
+
export interface SarifRun {
|
|
22
|
+
tool: {
|
|
23
|
+
driver: SarifDriver;
|
|
24
|
+
};
|
|
25
|
+
invocation?: SarifInvocation;
|
|
26
|
+
results: SarifResult[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* SARIF tool driver information.
|
|
30
|
+
*/
|
|
31
|
+
export interface SarifDriver {
|
|
32
|
+
name: string;
|
|
33
|
+
version: string;
|
|
34
|
+
informationUri: string;
|
|
35
|
+
rules: SarifRule[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* SARIF rule definition.
|
|
39
|
+
*/
|
|
40
|
+
export interface SarifRule {
|
|
41
|
+
id: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
shortDescription: {
|
|
44
|
+
text: string;
|
|
45
|
+
};
|
|
46
|
+
fullDescription?: {
|
|
47
|
+
text: string;
|
|
48
|
+
};
|
|
49
|
+
helpUri?: string;
|
|
50
|
+
defaultConfiguration?: {
|
|
51
|
+
level: SarifLevel;
|
|
52
|
+
};
|
|
53
|
+
properties?: {
|
|
54
|
+
precision?: "very-high" | "high" | "medium" | "low";
|
|
55
|
+
"security-severity"?: string;
|
|
56
|
+
category?: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* SARIF result (finding/issue).
|
|
61
|
+
*/
|
|
62
|
+
export interface SarifResult {
|
|
63
|
+
ruleId: string;
|
|
64
|
+
message: {
|
|
65
|
+
text: string;
|
|
66
|
+
};
|
|
67
|
+
level: SarifLevel;
|
|
68
|
+
locations: SarifLocation[];
|
|
69
|
+
partialFingerprints?: {
|
|
70
|
+
primaryLocationLineHash: string;
|
|
71
|
+
};
|
|
72
|
+
suppressions?: SarifSuppression[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* SARIF location for a result.
|
|
76
|
+
*/
|
|
77
|
+
export interface SarifLocation {
|
|
78
|
+
physicalLocation: {
|
|
79
|
+
artifactLocation: {
|
|
80
|
+
uri: string;
|
|
81
|
+
};
|
|
82
|
+
region?: {
|
|
83
|
+
startLine: number;
|
|
84
|
+
startColumn?: number;
|
|
85
|
+
endLine?: number;
|
|
86
|
+
endColumn?: number;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* SARIF suppression (for baselined issues).
|
|
92
|
+
*/
|
|
93
|
+
export interface SarifSuppression {
|
|
94
|
+
kind: "inSource" | "external";
|
|
95
|
+
justification?: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* SARIF invocation metadata.
|
|
99
|
+
*/
|
|
100
|
+
export interface SarifInvocation {
|
|
101
|
+
workingDirectory?: {
|
|
102
|
+
uri: string;
|
|
103
|
+
};
|
|
104
|
+
executionSuccessful: boolean;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* SARIF severity levels.
|
|
108
|
+
*/
|
|
109
|
+
export type SarifLevel = "none" | "note" | "warning" | "error";
|
|
110
|
+
/**
|
|
111
|
+
* Options for SARIF formatting.
|
|
112
|
+
*/
|
|
113
|
+
export interface SarifOptions {
|
|
114
|
+
/** Mark baselined issues as suppressed */
|
|
115
|
+
includeBaseline?: boolean;
|
|
116
|
+
/** Set of baselined fingerprints */
|
|
117
|
+
baselineFingerprints?: Set<string>;
|
|
118
|
+
/** Working directory for invocation */
|
|
119
|
+
workingDirectory?: string;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Audit result structure (matches factory.ts).
|
|
123
|
+
*/
|
|
124
|
+
export interface AuditResult {
|
|
125
|
+
job_id?: string;
|
|
126
|
+
status?: string;
|
|
127
|
+
url?: string;
|
|
128
|
+
mode?: string;
|
|
129
|
+
progress?: number;
|
|
130
|
+
created_at?: string;
|
|
131
|
+
started_at?: string;
|
|
132
|
+
completed_at?: string;
|
|
133
|
+
scores?: Record<string, unknown>;
|
|
134
|
+
issues?: unknown;
|
|
135
|
+
error?: string;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Format audit result as SARIF 2.1.0 JSON.
|
|
139
|
+
*
|
|
140
|
+
* @param result - Audit result from API
|
|
141
|
+
* @param options - Formatting options
|
|
142
|
+
* @returns SARIF 2.1.0 JSON string
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* const sarif = formatSarif(auditResult, {
|
|
147
|
+
* workingDirectory: process.cwd(),
|
|
148
|
+
* includeBaseline: true,
|
|
149
|
+
* baselineFingerprints: new Set(['abc123...'])
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export declare function formatSarif(result: AuditResult, options?: SarifOptions): string;
|
|
154
|
+
/**
|
|
155
|
+
* Format options for the SARIF formatter (compatible with FormatOptions pattern).
|
|
156
|
+
*/
|
|
157
|
+
export interface FormatSarifOptions {
|
|
158
|
+
sarif?: SarifOptions;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=sarif.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sarif.d.ts","sourceRoot":"","sources":["../../src/output/sarif.ts"],"names":[],"mappings":"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"}
|