@owasp-aghast/aghast 0.8.4 → 0.9.0-rc.1
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/dist/build-config.d.ts.map +1 -1
- package/dist/build-config.js +101 -2
- package/dist/build-config.js.map +1 -1
- package/dist/check-library.d.ts +25 -0
- package/dist/check-library.d.ts.map +1 -1
- package/dist/check-library.js +165 -0
- package/dist/check-library.js.map +1 -1
- package/dist/ci-metadata.d.ts +42 -0
- package/dist/ci-metadata.d.ts.map +1 -0
- package/dist/ci-metadata.js +131 -0
- package/dist/ci-metadata.js.map +1 -0
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/concurrency.d.ts +18 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +39 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/cost-tracker.d.ts +45 -0
- package/dist/cost-tracker.d.ts.map +1 -0
- package/dist/cost-tracker.js +56 -0
- package/dist/cost-tracker.js.map +1 -0
- package/dist/discoveries/glob-discovery.d.ts +21 -0
- package/dist/discoveries/glob-discovery.d.ts.map +1 -0
- package/dist/discoveries/glob-discovery.js +215 -0
- package/dist/discoveries/glob-discovery.js.map +1 -0
- package/dist/discoveries/script-discovery.d.ts +117 -0
- package/dist/discoveries/script-discovery.d.ts.map +1 -0
- package/dist/discoveries/script-discovery.js +593 -0
- package/dist/discoveries/script-discovery.js.map +1 -0
- package/dist/docs-url.d.ts +14 -0
- package/dist/docs-url.d.ts.map +1 -0
- package/dist/docs-url.js +17 -0
- package/dist/docs-url.js.map +1 -0
- package/dist/error-codes.d.ts +12 -1
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +16 -2
- package/dist/error-codes.js.map +1 -1
- package/dist/formatters/csv-formatter.d.ts +12 -1
- package/dist/formatters/csv-formatter.d.ts.map +1 -1
- package/dist/formatters/csv-formatter.js +64 -2
- package/dist/formatters/csv-formatter.js.map +1 -1
- package/dist/formatters/html-formatter.d.ts.map +1 -1
- package/dist/formatters/html-formatter.js +43 -3
- package/dist/formatters/html-formatter.js.map +1 -1
- package/dist/formatters/index.d.ts.map +1 -1
- package/dist/formatters/index.js +2 -0
- package/dist/formatters/index.js.map +1 -1
- package/dist/formatters/markdown-formatter.d.ts +61 -0
- package/dist/formatters/markdown-formatter.d.ts.map +1 -0
- package/dist/formatters/markdown-formatter.js +557 -0
- package/dist/formatters/markdown-formatter.js.map +1 -0
- package/dist/formatters/sarif-formatter.d.ts +7 -0
- package/dist/formatters/sarif-formatter.d.ts.map +1 -1
- package/dist/formatters/sarif-formatter.js +80 -0
- package/dist/formatters/sarif-formatter.js.map +1 -1
- package/dist/formatters/types.d.ts +24 -0
- package/dist/formatters/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +342 -9
- package/dist/index.js.map +1 -1
- package/dist/issue-file-writer.d.ts +21 -0
- package/dist/issue-file-writer.d.ts.map +1 -0
- package/dist/issue-file-writer.js +307 -0
- package/dist/issue-file-writer.js.map +1 -0
- package/dist/judge.d.ts +62 -0
- package/dist/judge.d.ts.map +1 -0
- package/dist/judge.js +305 -0
- package/dist/judge.js.map +1 -0
- package/dist/mock-agent-provider.d.ts +3 -0
- package/dist/mock-agent-provider.d.ts.map +1 -1
- package/dist/mock-agent-provider.js +13 -0
- package/dist/mock-agent-provider.js.map +1 -1
- package/dist/new-check.d.ts.map +1 -1
- package/dist/new-check.js +322 -13
- package/dist/new-check.js.map +1 -1
- package/dist/opencode-provider.d.ts.map +1 -1
- package/dist/opencode-provider.js +33 -0
- package/dist/opencode-provider.js.map +1 -1
- package/dist/repo-scan.d.ts +80 -0
- package/dist/repo-scan.d.ts.map +1 -0
- package/dist/repo-scan.js +250 -0
- package/dist/repo-scan.js.map +1 -0
- package/dist/result-handlers/pr-comment-handler.d.ts +136 -0
- package/dist/result-handlers/pr-comment-handler.d.ts.map +1 -0
- package/dist/result-handlers/pr-comment-handler.js +381 -0
- package/dist/result-handlers/pr-comment-handler.js.map +1 -0
- package/dist/retry.d.ts +128 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +261 -0
- package/dist/retry.js.map +1 -0
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +62 -0
- package/dist/runtime-config.js.map +1 -1
- package/dist/scan-runner.d.ts +31 -22
- package/dist/scan-runner.d.ts.map +1 -1
- package/dist/scan-runner.js +145 -100
- package/dist/scan-runner.js.map +1 -1
- package/dist/semgrep-runner.js +6 -4
- package/dist/semgrep-runner.js.map +1 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +4 -1
- package/dist/stats.js.map +1 -1
- package/dist/types.d.ts +149 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub PR comment result handler (Phase 1 of Spec E.7).
|
|
3
|
+
*
|
|
4
|
+
* Posts aghast scan findings as inline review comments on a GitHub pull request.
|
|
5
|
+
* Issues that fall outside the PR diff are skipped (with a debug log). Existing
|
|
6
|
+
* comments authored by aghast are deduplicated using a hidden HTML marker
|
|
7
|
+
* embedded in the comment body.
|
|
8
|
+
*
|
|
9
|
+
* Phase-1 limitations (intentional):
|
|
10
|
+
* - Multi-line findings (`endLine > startLine`) are anchored at `startLine`
|
|
11
|
+
* only; we do not yet emit GitHub's multi-line `start_line`/`line` form.
|
|
12
|
+
* - Dedup is best-effort and per-run: two parallel runs (e.g. CI matrix
|
|
13
|
+
* shards) on the same PR can each pass the dedup check and post duplicate
|
|
14
|
+
* comments. Confine `--pr` to a single matrix shard, or run after the
|
|
15
|
+
* matrix completes. A future phase could PATCH instead of POST.
|
|
16
|
+
*
|
|
17
|
+
* Future phases (issue tracker integration, AI remediation, IDE/LSP, Slack/email
|
|
18
|
+
* notifications) are intentionally out of scope here — see GitHub issue #119.
|
|
19
|
+
*/
|
|
20
|
+
import { spawn } from 'node:child_process';
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { appendFileSync } from 'node:fs';
|
|
23
|
+
import { logDebug, logProgress, logWarn } from '../logging.js';
|
|
24
|
+
const TAG = 'pr-comment-handler';
|
|
25
|
+
// ─── Default executor (real `gh` shell-out) ──────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Test-only fake executor selector.
|
|
28
|
+
*
|
|
29
|
+
* When `AGHAST_PR_COMMENT_FAKE_FILES` is set, `gh api .../pulls/<n>/files` calls
|
|
30
|
+
* resolve to the JSON contents of that file. When `AGHAST_PR_COMMENT_FAKE_LOG`
|
|
31
|
+
* is set, every captured call (including POST bodies) is appended as JSON-lines
|
|
32
|
+
* to that path. The real `gh` is never spawned. This is used by CLI integration
|
|
33
|
+
* tests so we can exercise the full --pr code path without hitting GitHub.
|
|
34
|
+
*/
|
|
35
|
+
function maybeFakeExecutor() {
|
|
36
|
+
const filesPath = process.env.AGHAST_PR_COMMENT_FAKE_FILES;
|
|
37
|
+
const logPath = process.env.AGHAST_PR_COMMENT_FAKE_LOG;
|
|
38
|
+
const commentsPath = process.env.AGHAST_PR_COMMENT_FAKE_COMMENTS;
|
|
39
|
+
if (!filesPath && !logPath && !commentsPath)
|
|
40
|
+
return undefined;
|
|
41
|
+
return async (command, args) => {
|
|
42
|
+
if (logPath) {
|
|
43
|
+
try {
|
|
44
|
+
appendFileSync(logPath, JSON.stringify({ command, args }) + '\n', 'utf-8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Best-effort logging only.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const apiPath = args[args.length - 1] ?? '';
|
|
51
|
+
if (/\/files(\?|$)/.test(apiPath) && filesPath) {
|
|
52
|
+
const { readFileSync } = await import('node:fs');
|
|
53
|
+
return { stdout: readFileSync(filesPath, 'utf-8'), stderr: '', exitCode: 0 };
|
|
54
|
+
}
|
|
55
|
+
if (/\/comments(\?|$)/.test(apiPath)) {
|
|
56
|
+
if (commentsPath) {
|
|
57
|
+
const { readFileSync } = await import('node:fs');
|
|
58
|
+
return { stdout: readFileSync(commentsPath, 'utf-8'), stderr: '', exitCode: 0 };
|
|
59
|
+
}
|
|
60
|
+
return { stdout: '[]', stderr: '', exitCode: 0 };
|
|
61
|
+
}
|
|
62
|
+
if (/\/reviews(\?|$)/.test(apiPath)) {
|
|
63
|
+
return { stdout: '{"id":1}', stderr: '', exitCode: 0 };
|
|
64
|
+
}
|
|
65
|
+
return { stdout: '[]', stderr: '', exitCode: 0 };
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export const defaultExecutor = (command, args, options = {}) => {
|
|
69
|
+
return new Promise((resolvePromise, reject) => {
|
|
70
|
+
const child = spawn(command, args, {
|
|
71
|
+
env: { ...process.env, ...(options.env ?? {}) },
|
|
72
|
+
shell: false,
|
|
73
|
+
});
|
|
74
|
+
let stdout = '';
|
|
75
|
+
let stderr = '';
|
|
76
|
+
child.stdout.on('data', (chunk) => {
|
|
77
|
+
stdout += chunk.toString('utf-8');
|
|
78
|
+
});
|
|
79
|
+
child.stderr.on('data', (chunk) => {
|
|
80
|
+
stderr += chunk.toString('utf-8');
|
|
81
|
+
});
|
|
82
|
+
child.on('error', (err) => {
|
|
83
|
+
// Translate the most common failure (gh CLI not installed) into an
|
|
84
|
+
// actionable message. Without this the user sees `Error: spawn gh ENOENT`
|
|
85
|
+
// and has to guess what to install.
|
|
86
|
+
if (err.code === 'ENOENT') {
|
|
87
|
+
reject(new Error(`gh CLI not found on PATH. Install it from https://cli.github.com or set GH_TOKEN and ensure 'gh' is reachable. (original: ${err.message})`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
reject(err);
|
|
91
|
+
});
|
|
92
|
+
child.on('close', (code) => {
|
|
93
|
+
resolvePromise({ stdout, stderr, exitCode: code ?? 1 });
|
|
94
|
+
});
|
|
95
|
+
if (options.input !== undefined) {
|
|
96
|
+
child.stdin.end(options.input);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
child.stdin.end();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
// ─── Patch parsing ───────────────────────────────────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* Parse a unified-diff `patch` (as returned by GitHub's PR files API) into the
|
|
106
|
+
* set of new-file line numbers that were added or modified by the PR.
|
|
107
|
+
*
|
|
108
|
+
* GitHub PR review comments can target either the LEFT (base) or RIGHT (head)
|
|
109
|
+
* side. We only post comments on lines that exist on the head side — i.e. lines
|
|
110
|
+
* actually changed (added) by this PR — to avoid leaving stale comments on
|
|
111
|
+
* unchanged code.
|
|
112
|
+
*/
|
|
113
|
+
export function parsePatchAddedLines(patch) {
|
|
114
|
+
const addedLines = new Set();
|
|
115
|
+
if (!patch)
|
|
116
|
+
return addedLines;
|
|
117
|
+
const lines = patch.split('\n');
|
|
118
|
+
let newLine = 0;
|
|
119
|
+
let inHunk = false;
|
|
120
|
+
// Hunk header format: @@ -<oldStart>,<oldLen> +<newStart>,<newLen> @@
|
|
121
|
+
const hunkRegex = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
const hunkMatch = hunkRegex.exec(line);
|
|
124
|
+
if (hunkMatch) {
|
|
125
|
+
newLine = Number(hunkMatch[1]);
|
|
126
|
+
inHunk = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// Defensive: ignore patch content (including stray `+` lines from a
|
|
130
|
+
// malformed patch) until we see the first hunk header. This prevents us
|
|
131
|
+
// from ever recording line 0 — which would later masquerade as a real
|
|
132
|
+
// diff line and either skip or anchor a comment at a nonexistent line.
|
|
133
|
+
if (!inHunk)
|
|
134
|
+
continue;
|
|
135
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
136
|
+
addedLines.add(newLine);
|
|
137
|
+
newLine++;
|
|
138
|
+
}
|
|
139
|
+
else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
140
|
+
// Removed line — does not advance newLine
|
|
141
|
+
}
|
|
142
|
+
else if (line.startsWith('\\')) {
|
|
143
|
+
// "" — ignore, doesn't advance
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Context line: present in both old and new.
|
|
147
|
+
newLine++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return addedLines;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build a map of file path → set of new-side line numbers changed by the PR.
|
|
154
|
+
*/
|
|
155
|
+
export function buildDiffLineMap(files) {
|
|
156
|
+
const map = new Map();
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
if (!file.patch)
|
|
159
|
+
continue;
|
|
160
|
+
map.set(file.filename, parsePatchAddedLines(file.patch));
|
|
161
|
+
}
|
|
162
|
+
return map;
|
|
163
|
+
}
|
|
164
|
+
// ─── Comment building ────────────────────────────────────────────────────────
|
|
165
|
+
/**
|
|
166
|
+
* Stable hash identifying a single (check + file + line + description) tuple.
|
|
167
|
+
* Embedded as an HTML comment in the body so future runs can recognise — and
|
|
168
|
+
* skip — comments they previously posted.
|
|
169
|
+
*/
|
|
170
|
+
export function issueHash(issue) {
|
|
171
|
+
// Description is deliberately NOT in the hash. AI-generated descriptions are
|
|
172
|
+
// non-deterministic (the LLM may rephrase the same finding across runs), so
|
|
173
|
+
// including the description would defeat dedup on re-runs. The trade-off: if
|
|
174
|
+
// an issue at the same (checkId, file, startLine) genuinely changes meaning,
|
|
175
|
+
// the original comment will not be refreshed — Phase 1 accepts that staleness
|
|
176
|
+
// in exchange for idempotent re-runs. A future phase can PATCH the existing
|
|
177
|
+
// comment to refresh stale descriptions.
|
|
178
|
+
const payload = [
|
|
179
|
+
issue.checkId,
|
|
180
|
+
issue.file,
|
|
181
|
+
issue.startLine,
|
|
182
|
+
].join('');
|
|
183
|
+
return createHash('sha256').update(payload).digest('hex').slice(0, 16);
|
|
184
|
+
}
|
|
185
|
+
const MARKER_PREFIX = '<!-- aghast-issue-id: ';
|
|
186
|
+
const MARKER_SUFFIX = ' -->';
|
|
187
|
+
export function buildCommentBody(issue) {
|
|
188
|
+
const hash = issueHash(issue);
|
|
189
|
+
const severity = issue.severity ? ` (${issue.severity})` : '';
|
|
190
|
+
const lines = [];
|
|
191
|
+
lines.push(`**aghast: ${issue.checkName}**${severity}`);
|
|
192
|
+
lines.push('');
|
|
193
|
+
lines.push(issue.description);
|
|
194
|
+
if (issue.recommendation) {
|
|
195
|
+
lines.push('');
|
|
196
|
+
lines.push(`**Recommendation:** ${issue.recommendation}`);
|
|
197
|
+
}
|
|
198
|
+
lines.push('');
|
|
199
|
+
lines.push(`${MARKER_PREFIX}${hash}${MARKER_SUFFIX}`);
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
}
|
|
202
|
+
export function extractMarkerHash(body) {
|
|
203
|
+
const idx = body.indexOf(MARKER_PREFIX);
|
|
204
|
+
if (idx === -1)
|
|
205
|
+
return undefined;
|
|
206
|
+
const start = idx + MARKER_PREFIX.length;
|
|
207
|
+
const end = body.indexOf(MARKER_SUFFIX, start);
|
|
208
|
+
if (end === -1)
|
|
209
|
+
return undefined;
|
|
210
|
+
return body.slice(start, end).trim();
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Map a SecurityIssue onto a ReviewComment, or return undefined if the issue's
|
|
214
|
+
* line is not part of the PR diff.
|
|
215
|
+
*/
|
|
216
|
+
export function issueToReviewComment(issue, diffLines) {
|
|
217
|
+
const fileLines = diffLines.get(issue.file);
|
|
218
|
+
if (!fileLines || !fileLines.has(issue.startLine)) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
path: issue.file,
|
|
223
|
+
line: issue.startLine,
|
|
224
|
+
side: 'RIGHT',
|
|
225
|
+
body: buildCommentBody(issue),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function ghApi(executor, path, options = {}) {
|
|
229
|
+
const args = ['api'];
|
|
230
|
+
if (options.method && options.method !== 'GET') {
|
|
231
|
+
args.push('--method', options.method);
|
|
232
|
+
}
|
|
233
|
+
if (options.paginate)
|
|
234
|
+
args.push('--paginate');
|
|
235
|
+
args.push('-H', 'Accept: application/vnd.github+json');
|
|
236
|
+
args.push(path);
|
|
237
|
+
let input;
|
|
238
|
+
if (options.body !== undefined) {
|
|
239
|
+
args.push('--input', '-');
|
|
240
|
+
input = JSON.stringify(options.body);
|
|
241
|
+
}
|
|
242
|
+
// Pass a token via env if provided; we never log or echo it.
|
|
243
|
+
const env = {};
|
|
244
|
+
if (options.token)
|
|
245
|
+
env.GH_TOKEN = options.token;
|
|
246
|
+
const result = await executor('gh', args, { input, env });
|
|
247
|
+
if (result.exitCode !== 0) {
|
|
248
|
+
throw new Error(`gh api ${path} failed (exit ${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}`);
|
|
249
|
+
}
|
|
250
|
+
if (result.stdout.trim() === '') {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
return JSON.parse(result.stdout);
|
|
254
|
+
}
|
|
255
|
+
async function listPullRequestFiles(executor, ctx) {
|
|
256
|
+
const path = `repos/${ctx.owner}/${ctx.repo}/pulls/${ctx.prNumber}/files?per_page=100`;
|
|
257
|
+
return ghApi(executor, path, {
|
|
258
|
+
paginate: true,
|
|
259
|
+
token: ctx.githubToken,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async function listExistingReviewComments(executor, ctx) {
|
|
263
|
+
const path = `repos/${ctx.owner}/${ctx.repo}/pulls/${ctx.prNumber}/comments?per_page=100`;
|
|
264
|
+
return ghApi(executor, path, {
|
|
265
|
+
paginate: true,
|
|
266
|
+
token: ctx.githubToken,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Upper bound on inline comments in a single review.
|
|
271
|
+
*
|
|
272
|
+
* A noisy check against a large diff could otherwise post hundreds of comments
|
|
273
|
+
* in one API call — unreviewable for the author, and large enough to risk the
|
|
274
|
+
* request being rejected outright, which loses every comment rather than some.
|
|
275
|
+
* Truncating is the lesser harm, and it is surfaced in the review body rather
|
|
276
|
+
* than dropped silently. Mirrors `limitTargets` in the SARIF parser.
|
|
277
|
+
*/
|
|
278
|
+
const DEFAULT_MAX_INLINE_COMMENTS = 50;
|
|
279
|
+
async function postReview(executor, ctx, comments, omitted = 0) {
|
|
280
|
+
const path = `repos/${ctx.owner}/${ctx.repo}/pulls/${ctx.prNumber}/reviews`;
|
|
281
|
+
const summary = `aghast posted ${comments.length} finding${comments.length === 1 ? '' : 's'}.`;
|
|
282
|
+
const body = {
|
|
283
|
+
event: 'COMMENT',
|
|
284
|
+
body: omitted > 0
|
|
285
|
+
? `${summary} ${omitted} further finding${omitted === 1 ? '' : 's'} ${omitted === 1 ? 'was' : 'were'} not posted inline (capped at ${comments.length} per review) — see the full scan report for the complete list.`
|
|
286
|
+
: summary,
|
|
287
|
+
comments,
|
|
288
|
+
};
|
|
289
|
+
if (ctx.headSha)
|
|
290
|
+
body.commit_id = ctx.headSha;
|
|
291
|
+
await ghApi(executor, path, {
|
|
292
|
+
method: 'POST',
|
|
293
|
+
body,
|
|
294
|
+
token: ctx.githubToken,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Post aghast scan issues as inline GitHub PR review comments.
|
|
299
|
+
*
|
|
300
|
+
* Workflow:
|
|
301
|
+
* 1. Fetch the PR's changed-file list and parse each file's diff hunks.
|
|
302
|
+
* 2. For each issue, compute the corresponding (path, line, RIGHT) target
|
|
303
|
+
* and skip issues whose line is outside the diff.
|
|
304
|
+
* 3. Fetch existing review comments and skip any whose hidden marker hash
|
|
305
|
+
* already matches an in-flight comment.
|
|
306
|
+
* 4. Post the remainder as a single review with `event=COMMENT`.
|
|
307
|
+
*/
|
|
308
|
+
export async function postPRComments(results, ctx, options = {}) {
|
|
309
|
+
const executor = options.executor ?? maybeFakeExecutor() ?? defaultExecutor;
|
|
310
|
+
const issues = results.issues ?? [];
|
|
311
|
+
if (issues.length === 0) {
|
|
312
|
+
logProgress(TAG, 'No issues to post — skipping PR comment phase');
|
|
313
|
+
return { posted: 0, skipped: 0 };
|
|
314
|
+
}
|
|
315
|
+
logProgress(TAG, `Posting up to ${issues.length} finding(s) to ${ctx.owner}/${ctx.repo}#${ctx.prNumber}`);
|
|
316
|
+
const files = await listPullRequestFiles(executor, ctx);
|
|
317
|
+
const diffLines = buildDiffLineMap(files);
|
|
318
|
+
logDebug(TAG, `PR diff covers ${diffLines.size} file(s) with patch hunks`);
|
|
319
|
+
// Map issues onto review comments + bucket the skipped ones.
|
|
320
|
+
const candidates = [];
|
|
321
|
+
let skippedOutsideDiff = 0;
|
|
322
|
+
for (const issue of issues) {
|
|
323
|
+
const comment = issueToReviewComment(issue, diffLines);
|
|
324
|
+
if (!comment) {
|
|
325
|
+
skippedOutsideDiff++;
|
|
326
|
+
logDebug(TAG, `Skipping issue outside PR diff: ${issue.checkId} ${issue.file}:${issue.startLine}`);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
candidates.push(comment);
|
|
330
|
+
}
|
|
331
|
+
if (candidates.length === 0) {
|
|
332
|
+
logProgress(TAG, `No issues fell within the PR diff (${skippedOutsideDiff} skipped)`);
|
|
333
|
+
return { posted: 0, skipped: skippedOutsideDiff };
|
|
334
|
+
}
|
|
335
|
+
// Deduplicate against existing review comments.
|
|
336
|
+
let existing = [];
|
|
337
|
+
try {
|
|
338
|
+
existing = await listExistingReviewComments(executor, ctx);
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
logWarn(TAG, `Failed to list existing PR comments — skipping dedup: ${err instanceof Error ? err.message : String(err)}`);
|
|
342
|
+
}
|
|
343
|
+
const existingHashes = new Set();
|
|
344
|
+
for (const c of existing) {
|
|
345
|
+
const h = extractMarkerHash(c.body);
|
|
346
|
+
if (h)
|
|
347
|
+
existingHashes.add(h);
|
|
348
|
+
}
|
|
349
|
+
const toPost = [];
|
|
350
|
+
let skippedDuplicate = 0;
|
|
351
|
+
for (const comment of candidates) {
|
|
352
|
+
const h = extractMarkerHash(comment.body);
|
|
353
|
+
if (h && existingHashes.has(h)) {
|
|
354
|
+
skippedDuplicate++;
|
|
355
|
+
logDebug(TAG, `Skipping duplicate comment ${h} on ${comment.path}:${comment.line}`);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
toPost.push(comment);
|
|
359
|
+
}
|
|
360
|
+
const totalSkipped = skippedOutsideDiff + skippedDuplicate;
|
|
361
|
+
if (toPost.length === 0) {
|
|
362
|
+
logProgress(TAG, `Nothing new to post (${skippedOutsideDiff} outside diff, ${skippedDuplicate} duplicate)`);
|
|
363
|
+
return { posted: 0, skipped: totalSkipped };
|
|
364
|
+
}
|
|
365
|
+
const maxComments = options.maxComments ?? DEFAULT_MAX_INLINE_COMMENTS;
|
|
366
|
+
const capped = toPost.slice(0, maxComments);
|
|
367
|
+
const omitted = toPost.length - capped.length;
|
|
368
|
+
if (omitted > 0) {
|
|
369
|
+
logWarn(TAG, `${toPost.length} comments to post exceeds the cap of ${maxComments}; ` +
|
|
370
|
+
`posting the first ${capped.length} and noting the remaining ${omitted} in the review summary. ` +
|
|
371
|
+
'All findings remain in the scan report.');
|
|
372
|
+
}
|
|
373
|
+
await postReview(executor, ctx, capped, omitted);
|
|
374
|
+
logProgress(TAG, `Posted ${capped.length} comment(s); skipped ${totalSkipped} (${skippedOutsideDiff} outside diff, ${skippedDuplicate} duplicate)`);
|
|
375
|
+
// Only present when something was actually omitted, so the result shape is
|
|
376
|
+
// unchanged for the overwhelmingly common case (and for existing consumers).
|
|
377
|
+
return omitted > 0
|
|
378
|
+
? { posted: capped.length, skipped: totalSkipped, omitted }
|
|
379
|
+
: { posted: capped.length, skipped: totalSkipped };
|
|
380
|
+
}
|
|
381
|
+
//# sourceMappingURL=pr-comment-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pr-comment-handler.js","sourceRoot":"","sources":["../../src/result-handlers/pr-comment-handler.ts"],"names":[],"mappings":"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"}
|
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry helper with exponential backoff and an optional shared circuit breaker.
|
|
3
|
+
*
|
|
4
|
+
* Spec Appendix E.6 — Retry and Resilience.
|
|
5
|
+
*
|
|
6
|
+
* Default classifier retries on rate-limit (429), 5xx, network/timeout errors;
|
|
7
|
+
* does NOT retry auth errors (401/403) or validation errors. Callers can override
|
|
8
|
+
* via `isRetryable`.
|
|
9
|
+
*
|
|
10
|
+
* The circuit breaker is shared across every `withRetry` invocation that
|
|
11
|
+
* receives the same `CircuitBreaker` instance. Within a single scan that
|
|
12
|
+
* means concurrent target analyses share one breaker; across multiple
|
|
13
|
+
* `runMultiScan` invocations the scope depends on whether the caller hoists
|
|
14
|
+
* the breaker to module scope or constructs a fresh one per scan (currently
|
|
15
|
+
* `runMultiScan` does the latter, so the breaker is per-scan, not per-process).
|
|
16
|
+
* When `consecutiveFailures` reaches the configured threshold, subsequent
|
|
17
|
+
* calls fail-fast with `CircuitOpenError` without invoking the wrapped
|
|
18
|
+
* function. A successful call resets the counter.
|
|
19
|
+
*/
|
|
20
|
+
export interface RetryOptions {
|
|
21
|
+
/** Maximum total attempts (initial + retries). Must be >= 1. */
|
|
22
|
+
maxAttempts: number;
|
|
23
|
+
/** Initial backoff delay in ms (before jitter). */
|
|
24
|
+
baseDelayMs: number;
|
|
25
|
+
/** Cap on the backoff delay in ms (before jitter). */
|
|
26
|
+
maxDelayMs: number;
|
|
27
|
+
/** Optional classifier — return true to retry, false to give up immediately. */
|
|
28
|
+
isRetryable?: (err: unknown) => boolean;
|
|
29
|
+
/** Optional shared circuit breaker. */
|
|
30
|
+
breaker?: CircuitBreaker;
|
|
31
|
+
/** Optional sleep override (for testing). Defaults to setTimeout. */
|
|
32
|
+
sleep?: (ms: number) => Promise<void>;
|
|
33
|
+
/** Optional jitter source returning [0, 1). Defaults to Math.random. */
|
|
34
|
+
random?: () => number;
|
|
35
|
+
/** Optional label for log lines. */
|
|
36
|
+
label?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Default retry options used when callers don't supply a value.
|
|
40
|
+
*
|
|
41
|
+
* `maxAttempts: 1` means retry is OFF unless explicitly configured — one
|
|
42
|
+
* attempt, no backoff, the original error rethrown untouched. This keeps a
|
|
43
|
+
* plain `aghast scan` behaving exactly as it did before retry existed.
|
|
44
|
+
* Opt in with `--retry-max-attempts`, `AGHAST_RETRY_MAX_ATTEMPTS`, or
|
|
45
|
+
* `retry.maxAttempts` in runtime config.
|
|
46
|
+
*
|
|
47
|
+
* The backoff values below are the defaults used *once retry is enabled*;
|
|
48
|
+
* they have no effect at `maxAttempts: 1`.
|
|
49
|
+
*/
|
|
50
|
+
export declare const DEFAULT_RETRY: Required<Pick<RetryOptions, 'maxAttempts' | 'baseDelayMs' | 'maxDelayMs'>>;
|
|
51
|
+
/** Attempts at or below this mean the caller has not opted into retry. */
|
|
52
|
+
export declare const RETRY_DISABLED_ATTEMPTS = 1;
|
|
53
|
+
/** True when the resolved options leave retry switched off. */
|
|
54
|
+
export declare function isRetryEnabled(maxAttempts: number): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Default retryable-error classifier.
|
|
57
|
+
*
|
|
58
|
+
* Decision order (first match wins):
|
|
59
|
+
* 1. HTTP status / statusCode property — authoritative when present.
|
|
60
|
+
* 4xx (except 429) → not retryable; 429 and 5xx → retryable.
|
|
61
|
+
* 2. errno-style `code` property (ECONNRESET, ETIMEDOUT, etc.) → retryable.
|
|
62
|
+
* 3. Free-text message fallback — only consulted when neither status nor
|
|
63
|
+
* code matched. Looks for explicit substrings like "rate limit",
|
|
64
|
+
* "timed out", "socket hang up". We deliberately do NOT pattern-match
|
|
65
|
+
* bare 5xx digits in messages (too prone to false positives — e.g.
|
|
66
|
+
* "failed at 500ms timeout" would otherwise be classified retryable).
|
|
67
|
+
*
|
|
68
|
+
* Does NOT retry:
|
|
69
|
+
* - 401 / 403 (auth)
|
|
70
|
+
* - 400 / 404 / 422 (client/validation)
|
|
71
|
+
* - Anything classified as fatal by the agent provider (the scan runner
|
|
72
|
+
* handles FatalProviderError separately — those never reach this classifier).
|
|
73
|
+
* - Unknown errors (callers can pass a custom `isRetryable` to widen this).
|
|
74
|
+
*/
|
|
75
|
+
export declare function defaultIsRetryable(err: unknown): boolean;
|
|
76
|
+
export interface CircuitBreakerOptions {
|
|
77
|
+
/** Maximum consecutive total failures before tripping. Must be >= 1. */
|
|
78
|
+
threshold: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Thrown when aghast's own per-call guard fires because a provider call did not
|
|
82
|
+
* return in time. Distinct from a provider-reported network timeout.
|
|
83
|
+
*
|
|
84
|
+
* Named rather than message-matched so the retry decision is deliberate: before
|
|
85
|
+
* this existed, the classification depended on the word "timeout" appearing in
|
|
86
|
+
* a free-text message, which is exactly the incidental matching the classifier
|
|
87
|
+
* warns against elsewhere.
|
|
88
|
+
*
|
|
89
|
+
* It IS retryable — a hung stream frequently succeeds on a fresh call — but the
|
|
90
|
+
* cost is visible: with `maxAttempts: n` a genuinely wedged provider takes up to
|
|
91
|
+
* n × the timeout to give up. That trade is documented in
|
|
92
|
+
* docs/configuration.md so anyone enabling retry knows what they are buying.
|
|
93
|
+
*/
|
|
94
|
+
export declare class AgentTimeoutError extends Error {
|
|
95
|
+
constructor(message: string);
|
|
96
|
+
}
|
|
97
|
+
/** Thrown by `withRetry` when the breaker is open (failing fast). */
|
|
98
|
+
export declare class CircuitOpenError extends Error {
|
|
99
|
+
constructor(consecutiveFailures: number, threshold: number);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Process-level circuit breaker. Shared across all `withRetry` calls that
|
|
103
|
+
* receive the same instance. Counts only TOTAL failures (after all retries
|
|
104
|
+
* for a single call exhausted), not individual attempts.
|
|
105
|
+
*/
|
|
106
|
+
export declare class CircuitBreaker {
|
|
107
|
+
private consecutiveFailures;
|
|
108
|
+
readonly threshold: number;
|
|
109
|
+
constructor(options: CircuitBreakerOptions);
|
|
110
|
+
/** True when the breaker is open (further calls should fail-fast). */
|
|
111
|
+
isOpen(): boolean;
|
|
112
|
+
/** Increment the failure counter. */
|
|
113
|
+
recordFailure(): void;
|
|
114
|
+
/** Reset the failure counter. Called on any successful `withRetry` call. */
|
|
115
|
+
recordSuccess(): void;
|
|
116
|
+
/** Force-reset the counter (useful for tests or manual recovery). */
|
|
117
|
+
reset(): void;
|
|
118
|
+
/** Current consecutive-failure count (for diagnostics/tests). */
|
|
119
|
+
getConsecutiveFailures(): number;
|
|
120
|
+
}
|
|
121
|
+
/** Compute the next backoff delay given an attempt number (1-based). */
|
|
122
|
+
export declare function computeBackoff(attempt: number, baseDelayMs: number, maxDelayMs: number, random?: () => number): number;
|
|
123
|
+
/**
|
|
124
|
+
* Run `fn` with retry on transient errors. Returns its result on success;
|
|
125
|
+
* throws on the final failure (or `CircuitOpenError` if the breaker is open).
|
|
126
|
+
*/
|
|
127
|
+
export declare function withRetry<T>(fn: () => Promise<T>, options: RetryOptions): Promise<T>;
|
|
128
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IACxC,uCAAuC;IACvC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,qEAAqE;IACrE,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,aAAa,GAAG,aAAa,GAAG,YAAY,CAAC,CAIpG,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAkExD;AAID,MAAM,WAAW,qBAAqB;IACpC,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED,qEAAqE;AACrE,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,mBAAmB,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAM3D;AAED;;;;GAIG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,mBAAmB,CAAK;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,OAAO,EAAE,qBAAqB;IAO1C,sEAAsE;IACtE,MAAM,IAAI,OAAO;IAIjB,qCAAqC;IACrC,aAAa,IAAI,IAAI;IAIrB,4EAA4E;IAC5E,aAAa,IAAI,IAAI;IAIrB,qEAAqE;IACrE,KAAK,IAAI,IAAI;IAIb,iEAAiE;IACjE,sBAAsB,IAAI,MAAM;CAGjC;AAID,wEAAwE;AACxE,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAM,MAAoB,GACjC,MAAM,CAOR;AAKD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,CAiE1F"}
|