@owasp-aghast/aghast 0.8.3 → 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/config/pricing.json +39 -9
- 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 +172 -1
- 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/claude-code-provider.d.ts +7 -0
- package/dist/claude-code-provider.d.ts.map +1 -1
- package/dist/claude-code-provider.js +65 -1
- package/dist/claude-code-provider.js.map +1 -1
- package/dist/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 +45 -0
- package/dist/formatters/csv-formatter.d.ts.map +1 -0
- package/dist/formatters/csv-formatter.js +176 -0
- package/dist/formatters/csv-formatter.js.map +1 -0
- package/dist/formatters/html-formatter.d.ts +39 -0
- package/dist/formatters/html-formatter.d.ts.map +1 -0
- package/dist/formatters/html-formatter.js +432 -0
- package/dist/formatters/html-formatter.js.map +1 -0
- package/dist/formatters/index.d.ts.map +1 -1
- package/dist/formatters/index.js +6 -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 +365 -10
- 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 +334 -22
- 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
package/dist/index.js
CHANGED
|
@@ -8,11 +8,13 @@ import { resolve, dirname } from 'node:path';
|
|
|
8
8
|
import { runMultiScanWithCost } from './scan-runner.js';
|
|
9
9
|
import { loadDefaultPricing, mergePricing, formatCostSourceLabel } from './cost-calculator.js';
|
|
10
10
|
import { saveScanRecord, queryScanHistory } from './scan-history.js';
|
|
11
|
+
import { writeIndividualIssueFiles } from './issue-file-writer.js';
|
|
11
12
|
import { createProviderByName, getProviderNames, DEFAULT_PROVIDER_NAME } from './provider-registry.js';
|
|
12
|
-
import { loadCheckRegistry, discoverCheckFolders, resolveChecks,
|
|
13
|
+
import { loadCheckRegistry, discoverCheckFolders, resolveChecks, filterChecksForRepositoryAsync, sortChecksByPriority, validateCheck, loadCheckDetails, } from './check-library.js';
|
|
14
|
+
import { clearRepoSnapshotCache } from './repo-scan.js';
|
|
13
15
|
import { analyzeRepository } from './repository-analyzer.js';
|
|
14
16
|
import { loadRuntimeConfig } from './runtime-config.js';
|
|
15
|
-
import { logProgress, logDebug, setLogLevel, createTimer, isValidLogLevel, initFileHandler, closeAllHandlers, getAvailableLogTypes } from './logging.js';
|
|
17
|
+
import { logProgress, logDebug, logWarn, setLogLevel, createTimer, isValidLogLevel, initFileHandler, closeAllHandlers, getAvailableLogTypes } from './logging.js';
|
|
16
18
|
import { MOCK_MODEL_NAME, DEFAULT_MODEL } from './types.js';
|
|
17
19
|
import { getFormatter } from './formatters/index.js';
|
|
18
20
|
import { verifySemgrepInstalled } from './semgrep-runner.js';
|
|
@@ -21,11 +23,36 @@ import { verifyOpenAntInstalled } from './openant-runner.js';
|
|
|
21
23
|
import { MockAgentProvider } from './mock-agent-provider.js';
|
|
22
24
|
import { ERROR_CODES, formatError, formatFatalError } from './error-codes.js';
|
|
23
25
|
import { DEFAULT_OUTPUT_FORMAT, DEFAULT_LOG_LEVEL, DEFAULT_LOG_TYPE } from './defaults.js';
|
|
26
|
+
import { docsFooter } from './docs-url.js';
|
|
24
27
|
import { colorStatus } from './colors.js';
|
|
25
28
|
import { getCheckType } from './check-types.js';
|
|
26
29
|
import { getDiscovery, getRegisteredDiscoveries } from './discovery.js';
|
|
30
|
+
import { postPRComments } from './result-handlers/pr-comment-handler.js';
|
|
27
31
|
import { createRequire } from 'node:module';
|
|
28
32
|
const TAG = 'aghast';
|
|
33
|
+
async function createMockJudgeProvider(modelOverride) {
|
|
34
|
+
// AGHAST_MOCK_JUDGE='true' → default TP response; AGHAST_MOCK_JUDGE=<path> → read from that file
|
|
35
|
+
const mockJudgeValue = process.env.AGHAST_MOCK_JUDGE;
|
|
36
|
+
let rawResponse = '{"verdict":"true_positive","confidence":1.0,"rationale":"mock"}';
|
|
37
|
+
if (mockJudgeValue && mockJudgeValue !== 'true') {
|
|
38
|
+
try {
|
|
39
|
+
rawResponse = await readFile(resolve(mockJudgeValue), 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
throw new Error(formatError(ERROR_CODES.E8001, `path: ${mockJudgeValue}`), { cause: err });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Test hook mirroring AGHAST_MOCK_FAIL_TIMES, but for the judge's own calls.
|
|
46
|
+
// Kept separate so a test can fail the judge without also failing the scan
|
|
47
|
+
// (and vice versa) — the two are retried independently.
|
|
48
|
+
const failTimesRaw = process.env.AGHAST_MOCK_JUDGE_FAIL_TIMES;
|
|
49
|
+
const parsedFailTimes = failTimesRaw ? Number(failTimesRaw) : 0;
|
|
50
|
+
const failTimes = Number.isInteger(parsedFailTimes) && parsedFailTimes > 0 ? parsedFailTimes : 0;
|
|
51
|
+
const provider = new MockAgentProvider({ rawResponse, failTimes });
|
|
52
|
+
await provider.initialize({});
|
|
53
|
+
provider.setModel?.(modelOverride ?? MOCK_MODEL_NAME);
|
|
54
|
+
return provider;
|
|
55
|
+
}
|
|
29
56
|
async function createMockProvider() {
|
|
30
57
|
// AGHAST_MOCK_AI='true' → default empty response; AGHAST_MOCK_AI=<path> → read from that file
|
|
31
58
|
const mockAiValue = process.env.AGHAST_MOCK_AI;
|
|
@@ -62,7 +89,13 @@ async function createMockProvider() {
|
|
|
62
89
|
};
|
|
63
90
|
}
|
|
64
91
|
}
|
|
65
|
-
|
|
92
|
+
// Test hook: AGHAST_MOCK_FAIL_TIMES=<n> makes the mock fail its first n calls
|
|
93
|
+
// with a retryable (503) error before succeeding, so retry behaviour can be
|
|
94
|
+
// exercised end-to-end through the real CLI.
|
|
95
|
+
const failTimesRaw = process.env.AGHAST_MOCK_FAIL_TIMES;
|
|
96
|
+
const parsedFailTimes = failTimesRaw ? Number(failTimesRaw) : 0;
|
|
97
|
+
const failTimes = Number.isInteger(parsedFailTimes) && parsedFailTimes > 0 ? parsedFailTimes : 0;
|
|
98
|
+
const provider = new MockAgentProvider({ rawResponse, tokenUsage, failTimes });
|
|
66
99
|
await provider.initialize({});
|
|
67
100
|
return provider;
|
|
68
101
|
}
|
|
@@ -74,12 +107,13 @@ Arguments:
|
|
|
74
107
|
<repo-path> Path to the repository to scan
|
|
75
108
|
|
|
76
109
|
General options:
|
|
110
|
+
-h, --help Show this help message
|
|
77
111
|
--config-dir <path> Config directory containing checks-config.json,
|
|
78
112
|
checks/ folder, and optionally runtime-config.json.
|
|
79
113
|
Required unless AGHAST_CONFIG_DIR is set.
|
|
80
114
|
--output <path> Output file path for results
|
|
81
115
|
(default: <repo-path>/security_checks_results.<ext>)
|
|
82
|
-
--output-format json
|
|
116
|
+
--output-format <fmt> Output format: json, sarif, csv, html, markdown (default: json)
|
|
83
117
|
--fail-on-check-failure Exit with code 1 if any check FAILs or ERRORs
|
|
84
118
|
--debug Shorthand for --log-level debug
|
|
85
119
|
--log-level <level> Console log level: error, warn, info, debug, trace
|
|
@@ -91,6 +125,7 @@ General options:
|
|
|
91
125
|
--model <model> AI model override (e.g. claude-sonnet-4-20250514)
|
|
92
126
|
--agent-provider <name> Agent provider name (default: claude-code)
|
|
93
127
|
--generic-prompt <file> Generic prompt template filename in prompts/ dir
|
|
128
|
+
--runtime-config <path> Path to runtime config file
|
|
94
129
|
--diff-ref <ref> Git ref to diff against (e.g. HEAD~1, main, SHA).
|
|
95
130
|
Auto-activates diff filtering on every check whose
|
|
96
131
|
discovery supports it, unless the check opts out
|
|
@@ -102,6 +137,24 @@ General options:
|
|
|
102
137
|
--budget-limit-tokens <n> Abort the scan when accumulated tokens exceed n.
|
|
103
138
|
Warns at 80%, aborts at 100%
|
|
104
139
|
|
|
140
|
+
PR comment options (post findings as inline GitHub PR review comments):
|
|
141
|
+
--pr <number> Pull request number to post comments on
|
|
142
|
+
--repo <owner/repo> GitHub repository in owner/repo form
|
|
143
|
+
(default: $GITHUB_REPOSITORY)
|
|
144
|
+
--base-sha <sha> Base commit SHA (default: $GITHUB_BASE_SHA)
|
|
145
|
+
--head-sha <sha> Head commit SHA
|
|
146
|
+
(default: $GITHUB_HEAD_SHA / $GITHUB_SHA)
|
|
147
|
+
Judge stage options (enables a post-scan LLM re-evaluation of findings):
|
|
148
|
+
--judge-model <model> Enable the judge stage using this model. Required
|
|
149
|
+
to activate the stage (no separate --judge flag).
|
|
150
|
+
--judge-provider <name> Agent provider for the judge (default: scan provider)
|
|
151
|
+
--judge-concurrency <n> Max concurrent judge calls (default: 5)
|
|
152
|
+
--judge-drop-false-positives Remove issues judged as false positives from output
|
|
153
|
+
--judge-min-confidence <f> Demote true_positive verdicts with confidence < this
|
|
154
|
+
value to uncertain (0.0–1.0)
|
|
155
|
+
--retry-max-attempts <n> Retry transient provider failures up to n attempts
|
|
156
|
+
per AI call (default: 1, i.e. no retry)
|
|
157
|
+
|
|
105
158
|
Environment variables:
|
|
106
159
|
ANTHROPIC_API_KEY API key for Claude. If unset, AI-based checks fall
|
|
107
160
|
back to a logged-in local Claude session
|
|
@@ -112,16 +165,34 @@ Environment variables:
|
|
|
112
165
|
AGHAST_LOG_LEVEL Console log level (CLI --log-level takes precedence)
|
|
113
166
|
AGHAST_LOG_FILE Log file path (CLI --log-file takes precedence)
|
|
114
167
|
AGHAST_LOG_TYPE Log file handler type (CLI --log-type takes precedence)
|
|
168
|
+
AGHAST_MOCK_SARIF Use a SARIF file instead of running Semgrep or Opengrep
|
|
169
|
+
(test/development use only)
|
|
170
|
+
AGHAST_OPENANT_DATASET Use a pre-generated OpenAnt dataset JSON file
|
|
115
171
|
AGHAST_DIFF_REF Git ref to diff against (CLI --diff-ref takes precedence)
|
|
172
|
+
NO_COLOR Set to "1" to disable colored output
|
|
173
|
+
GITHUB_REPOSITORY Default for --repo (auto-set in GitHub Actions)
|
|
174
|
+
GITHUB_BASE_SHA Default for --base-sha
|
|
175
|
+
GITHUB_HEAD_SHA Default for --head-sha
|
|
176
|
+
GITHUB_SHA Fallback for --head-sha (auto-set in GitHub Actions)
|
|
177
|
+
GH_TOKEN / GITHUB_TOKEN Auth for the gh CLI when posting PR comments
|
|
178
|
+
AGHAST_JUDGE_MODEL Judge model (presence enables the stage; CLI --judge-model takes precedence)
|
|
179
|
+
AGHAST_JUDGE_PROVIDER Judge provider (CLI --judge-provider takes precedence)
|
|
180
|
+
AGHAST_RETRY_MAX_ATTEMPTS Retry attempts per AI call; >1 enables retry
|
|
181
|
+
(CLI --retry-max-attempts takes precedence)
|
|
182
|
+
AGHAST_MOCK_JUDGE Enables mock judge provider. Set to "true" for default
|
|
183
|
+
{"verdict":"true_positive","confidence":1.0,"rationale":"mock"},
|
|
184
|
+
or set to a file path for a custom fixture
|
|
116
185
|
|
|
117
186
|
Examples:
|
|
118
187
|
aghast scan ./my-repo --config-dir ./my-checks
|
|
119
188
|
aghast scan ./my-repo --config-dir ./my-checks --output results.sarif --output-format sarif
|
|
120
189
|
aghast scan ./my-repo --config-dir ./my-checks --fail-on-check-failure --debug
|
|
121
190
|
aghast scan ./my-repo --config-dir ./my-checks --log-file scan.log --log-level warn
|
|
122
|
-
aghast scan ./my-repo --config-dir ./my-checks --model claude-sonnet-4-20250514
|
|
191
|
+
aghast scan ./my-repo --config-dir ./my-checks --model claude-sonnet-4-20250514
|
|
192
|
+
|
|
193
|
+
${docsFooter('scanning.md')}`;
|
|
123
194
|
function parseArgs(args) {
|
|
124
|
-
if (args.length < 1 || args
|
|
195
|
+
if (args.length < 1 || args.includes('--help') || args.includes('-h')) {
|
|
125
196
|
console.log(SCAN_HELP);
|
|
126
197
|
process.exit(0);
|
|
127
198
|
}
|
|
@@ -145,6 +216,16 @@ function parseArgs(args) {
|
|
|
145
216
|
let diffFile;
|
|
146
217
|
let budgetLimitCost;
|
|
147
218
|
let budgetLimitTokens;
|
|
219
|
+
let prNumber;
|
|
220
|
+
let prRepo;
|
|
221
|
+
let baseSha;
|
|
222
|
+
let headSha;
|
|
223
|
+
let judgeModel;
|
|
224
|
+
let judgeProvider;
|
|
225
|
+
let judgeConcurrency;
|
|
226
|
+
const judgeDropFalsePositives = args.includes('--judge-drop-false-positives');
|
|
227
|
+
let judgeMinConfidence;
|
|
228
|
+
let retryMaxAttempts;
|
|
148
229
|
for (let i = startIdx; i < args.length; i++) {
|
|
149
230
|
switch (args[i]) {
|
|
150
231
|
case '--config-dir': {
|
|
@@ -290,7 +371,126 @@ function parseArgs(args) {
|
|
|
290
371
|
i++;
|
|
291
372
|
break;
|
|
292
373
|
}
|
|
293
|
-
|
|
374
|
+
case '--pr': {
|
|
375
|
+
const raw = args[i + 1];
|
|
376
|
+
if (!raw) {
|
|
377
|
+
console.error(formatError(ERROR_CODES.E1001, '--pr requires a pull request number'));
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
const parsedNum = Number.parseInt(raw, 10);
|
|
381
|
+
if (!Number.isInteger(parsedNum) || parsedNum <= 0) {
|
|
382
|
+
console.error(formatError(ERROR_CODES.E1001, `--pr requires a positive integer (got "${raw}")`));
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
prNumber = parsedNum;
|
|
386
|
+
i++;
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
case '--repo': {
|
|
390
|
+
prRepo = args[i + 1];
|
|
391
|
+
if (!prRepo) {
|
|
392
|
+
console.error(formatError(ERROR_CODES.E1001, '--repo requires an owner/repo argument'));
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
i++;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
case '--base-sha': {
|
|
399
|
+
baseSha = args[i + 1];
|
|
400
|
+
if (!baseSha) {
|
|
401
|
+
console.error(formatError(ERROR_CODES.E1001, '--base-sha requires a SHA argument'));
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
i++;
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
case '--head-sha': {
|
|
408
|
+
headSha = args[i + 1];
|
|
409
|
+
if (!headSha) {
|
|
410
|
+
console.error(formatError(ERROR_CODES.E1001, '--head-sha requires a SHA argument'));
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
i++;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
case '--judge-model': {
|
|
417
|
+
judgeModel = args[i + 1];
|
|
418
|
+
if (!judgeModel) {
|
|
419
|
+
console.error(formatError(ERROR_CODES.E1001, '--judge-model requires a model name argument'));
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
i++;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case '--judge-provider': {
|
|
426
|
+
judgeProvider = args[i + 1];
|
|
427
|
+
if (!judgeProvider) {
|
|
428
|
+
console.error(formatError(ERROR_CODES.E1001, '--judge-provider requires a provider name argument'));
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
i++;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
case '--judge-concurrency': {
|
|
435
|
+
const raw = args[i + 1];
|
|
436
|
+
if (!raw) {
|
|
437
|
+
console.error(formatError(ERROR_CODES.E1001, '--judge-concurrency requires a number argument'));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
const n = Number(raw);
|
|
441
|
+
if (!Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) {
|
|
442
|
+
console.error(formatError(ERROR_CODES.E1001, `--judge-concurrency must be a positive integer (got "${raw}")`));
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
judgeConcurrency = n;
|
|
446
|
+
i++;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case '--judge-min-confidence': {
|
|
450
|
+
const raw = args[i + 1];
|
|
451
|
+
if (!raw) {
|
|
452
|
+
console.error(formatError(ERROR_CODES.E1001, '--judge-min-confidence requires a number argument (0.0–1.0)'));
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
const n = Number(raw);
|
|
456
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
457
|
+
console.error(formatError(ERROR_CODES.E1001, `--judge-min-confidence must be between 0 and 1 (got "${raw}")`));
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
judgeMinConfidence = n;
|
|
461
|
+
i++;
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
case '--retry-max-attempts': {
|
|
465
|
+
const raw = args[i + 1];
|
|
466
|
+
if (!raw) {
|
|
467
|
+
console.error(formatError(ERROR_CODES.E1001, '--retry-max-attempts requires a number argument'));
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
const n = Number(raw);
|
|
471
|
+
if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
|
|
472
|
+
console.error(formatError(ERROR_CODES.E1001, `--retry-max-attempts must be an integer >= 1 (got "${raw}")`));
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
retryMaxAttempts = n;
|
|
476
|
+
i++;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
// Boolean flags: their values are read via args.includes() above, but each
|
|
480
|
+
// still needs a case here — without one it reaches `default` and is
|
|
481
|
+
// rejected as an unknown option.
|
|
482
|
+
case '--fail-on-check-failure':
|
|
483
|
+
case '--debug':
|
|
484
|
+
case '--judge-drop-false-positives':
|
|
485
|
+
break;
|
|
486
|
+
default: {
|
|
487
|
+
const arg = args[i];
|
|
488
|
+
if (arg?.startsWith('--')) {
|
|
489
|
+
const suggestion = arg === '--provider' ? ' Did you mean --agent-provider?' : '';
|
|
490
|
+
console.error(formatError(ERROR_CODES.E1002, `Unknown option: ${arg}.${suggestion}`));
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
294
494
|
}
|
|
295
495
|
}
|
|
296
496
|
return {
|
|
@@ -299,8 +499,36 @@ function parseArgs(args) {
|
|
|
299
499
|
runtimeConfigPath, model, agentProvider, genericPrompt,
|
|
300
500
|
diffRef, diffFile,
|
|
301
501
|
budgetLimitCost, budgetLimitTokens,
|
|
502
|
+
prNumber, prRepo, baseSha, headSha,
|
|
503
|
+
judgeModel, judgeProvider, judgeConcurrency,
|
|
504
|
+
judgeDropFalsePositives: judgeDropFalsePositives || undefined,
|
|
505
|
+
judgeMinConfidence,
|
|
506
|
+
retryMaxAttempts,
|
|
302
507
|
};
|
|
303
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Parse "owner/repo" into its parts. Returns undefined when the input is
|
|
511
|
+
* missing or malformed so callers can produce a CLI-friendly error.
|
|
512
|
+
*
|
|
513
|
+
* Validates each half against `^[A-Za-z0-9_.-]+$` — the character set GitHub
|
|
514
|
+
* actually permits in owner/repo names. This rejects whitespace and shell
|
|
515
|
+
* metacharacters defensively, even though the parsed value is only ever
|
|
516
|
+
* interpolated into a `gh api` URL path (no shell).
|
|
517
|
+
*/
|
|
518
|
+
const REPO_SLUG_PART = /^[A-Za-z0-9_.-]+$/;
|
|
519
|
+
export function parseRepoSlug(slug) {
|
|
520
|
+
if (!slug)
|
|
521
|
+
return undefined;
|
|
522
|
+
const parts = slug.split('/');
|
|
523
|
+
if (parts.length !== 2)
|
|
524
|
+
return undefined;
|
|
525
|
+
const [owner, repo] = parts;
|
|
526
|
+
if (!owner || !repo)
|
|
527
|
+
return undefined;
|
|
528
|
+
if (!REPO_SLUG_PART.test(owner) || !REPO_SLUG_PART.test(repo))
|
|
529
|
+
return undefined;
|
|
530
|
+
return { owner, repo };
|
|
531
|
+
}
|
|
304
532
|
async function createProvider(useMock, agentProviderName, modelOverride) {
|
|
305
533
|
if (useMock) {
|
|
306
534
|
logProgress(TAG, `Mock provider enabled via AGHAST_MOCK_AI=${process.env.AGHAST_MOCK_AI}`);
|
|
@@ -359,6 +587,16 @@ async function validateConfigDir(configDir) {
|
|
|
359
587
|
export async function runScan(args) {
|
|
360
588
|
const globalTimer = createTimer();
|
|
361
589
|
const parsed = parseArgs(args);
|
|
590
|
+
// Reset the per-process repo-snapshot cache so back-to-back programmatic
|
|
591
|
+
// invocations of `runScan` don't reuse a stale filesystem snapshot from a
|
|
592
|
+
// previous run (e.g. when callers edit the target repo between scans).
|
|
593
|
+
//
|
|
594
|
+
// Caveat: the cache is module-scoped, so two `runScan` invocations running
|
|
595
|
+
// *concurrently* in the same Node process will clobber each other's cached
|
|
596
|
+
// snapshots — each scan still works correctly, it just won't share the
|
|
597
|
+
// filesystem walk across invocations. Sequential invocations are the
|
|
598
|
+
// primary supported pattern.
|
|
599
|
+
clearRepoSnapshotCache();
|
|
362
600
|
// --config-dir is required (CLI flag > AGHAST_CONFIG_DIR env var)
|
|
363
601
|
const rawConfigDir = parsed.configDir || process.env.AGHAST_CONFIG_DIR;
|
|
364
602
|
if (!rawConfigDir) {
|
|
@@ -441,7 +679,9 @@ export async function runScan(args) {
|
|
|
441
679
|
const effectiveRepoPath = parsed.repositoryPath;
|
|
442
680
|
const repoAnalysis = await analyzeRepository(effectiveRepoPath);
|
|
443
681
|
const repoUrl = repoAnalysis?.repository.remoteUrl ?? effectiveRepoPath;
|
|
444
|
-
const
|
|
682
|
+
const filtered = await filterChecksForRepositoryAsync(allChecks, repoUrl, effectiveRepoPath);
|
|
683
|
+
// Run lower-priority checks first; checks without a priority sort to the end.
|
|
684
|
+
const matchingChecks = sortChecksByPriority(filtered);
|
|
445
685
|
logProgress(TAG, `Found ${matchingChecks.length} matching checks (of ${allChecks.length} total)`);
|
|
446
686
|
if (matchingChecks.length === 0) {
|
|
447
687
|
logProgress(TAG, 'No matching checks found for this repository');
|
|
@@ -452,6 +692,11 @@ export async function runScan(args) {
|
|
|
452
692
|
// instructionsFile is already absolute from resolveChecks — validate against ''
|
|
453
693
|
const validation = await validateCheck(check, '');
|
|
454
694
|
if (!validation.valid) {
|
|
695
|
+
const emptyInstructionsError = validation.errors.find((error) => error.includes('Instructions file') && error.includes('is empty'));
|
|
696
|
+
if (emptyInstructionsError) {
|
|
697
|
+
console.error(formatError(ERROR_CODES.E2004, `Invalid check "${check.id}": ${emptyInstructionsError}`));
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
455
700
|
logProgress(TAG, `Skipping invalid check "${check.id}": ${validation.errors.join(', ')}`);
|
|
456
701
|
continue;
|
|
457
702
|
}
|
|
@@ -627,8 +872,67 @@ export async function runScan(args) {
|
|
|
627
872
|
logDebug(TAG, `Could not load scan history for budget check: ${err instanceof Error ? err.message : String(err)}`);
|
|
628
873
|
}
|
|
629
874
|
}
|
|
875
|
+
// ─── Judge stage setup ───
|
|
876
|
+
// Stage is enabled iff a judge model is resolvable from any config source.
|
|
877
|
+
// Provider defaults to the scan provider if not explicitly set (decision #8).
|
|
878
|
+
const resolvedJudgeModel = parsed.judgeModel ?? process.env.AGHAST_JUDGE_MODEL ?? runtimeConfig.judge?.model;
|
|
879
|
+
const resolvedJudgeProvider = parsed.judgeProvider ?? process.env.AGHAST_JUDGE_PROVIDER ?? runtimeConfig.judge?.provider;
|
|
880
|
+
const resolvedJudgeConcurrency = parsed.judgeConcurrency ?? runtimeConfig.judge?.concurrency;
|
|
881
|
+
const resolvedJudgeDropFP = parsed.judgeDropFalsePositives ?? runtimeConfig.judge?.dropFalsePositives;
|
|
882
|
+
const resolvedJudgeMinConf = parsed.judgeMinConfidence ?? runtimeConfig.judge?.minConfidence;
|
|
883
|
+
const mockJudgeEnv = process.env.AGHAST_MOCK_JUDGE;
|
|
884
|
+
const useMockJudge = !!(mockJudgeEnv && mockJudgeEnv !== 'false');
|
|
885
|
+
let judgeOptions;
|
|
886
|
+
if (resolvedJudgeModel) {
|
|
887
|
+
const judgeProviderName = resolvedJudgeProvider ?? agentProviderName;
|
|
888
|
+
let judgeProvider;
|
|
889
|
+
if (useMockJudge) {
|
|
890
|
+
logProgress(TAG, `Mock judge provider enabled via AGHAST_MOCK_JUDGE=${mockJudgeEnv}`);
|
|
891
|
+
judgeProvider = await createMockJudgeProvider(resolvedJudgeModel);
|
|
892
|
+
}
|
|
893
|
+
else if (useMock) {
|
|
894
|
+
// If scan uses mock, use mock for judge too (tests that set AGHAST_MOCK_AI but not AGHAST_MOCK_JUDGE)
|
|
895
|
+
logWarn(TAG, 'AGHAST_MOCK_AI is set; judge provider will also use mock (set AGHAST_MOCK_JUDGE to control judge response)');
|
|
896
|
+
judgeProvider = await createMockJudgeProvider(resolvedJudgeModel);
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
const jp = createProviderByName(judgeProviderName);
|
|
900
|
+
await jp.initialize({ apiKey: process.env.ANTHROPIC_API_KEY, model: resolvedJudgeModel });
|
|
901
|
+
judgeProvider = jp;
|
|
902
|
+
}
|
|
903
|
+
judgeOptions = {
|
|
904
|
+
provider: judgeProvider,
|
|
905
|
+
providerName: useMockJudge || useMock ? 'mock' : judgeProviderName,
|
|
906
|
+
model: resolvedJudgeModel,
|
|
907
|
+
concurrency: resolvedJudgeConcurrency,
|
|
908
|
+
dropFalsePositives: resolvedJudgeDropFP,
|
|
909
|
+
minConfidence: resolvedJudgeMinConf,
|
|
910
|
+
};
|
|
911
|
+
logProgress(TAG, `Judge stage enabled: model=${resolvedJudgeModel}, provider=${judgeOptions.providerName}`);
|
|
912
|
+
}
|
|
913
|
+
// Retry is opt-in. Precedence matches every other setting:
|
|
914
|
+
// CLI --retry-max-attempts > AGHAST_RETRY_MAX_ATTEMPTS > runtime config.
|
|
915
|
+
// When none is supplied this stays undefined and the scan runner's default
|
|
916
|
+
// (one attempt, no backoff, no circuit breaker) applies.
|
|
917
|
+
let resolvedRetry = runtimeConfig.retry;
|
|
918
|
+
const envRetryAttempts = process.env.AGHAST_RETRY_MAX_ATTEMPTS;
|
|
919
|
+
let retryAttemptsOverride = parsed.retryMaxAttempts;
|
|
920
|
+
if (retryAttemptsOverride === undefined && envRetryAttempts !== undefined) {
|
|
921
|
+
const n = Number(envRetryAttempts);
|
|
922
|
+
if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
|
|
923
|
+
console.error(formatError(ERROR_CODES.E1001, `AGHAST_RETRY_MAX_ATTEMPTS must be an integer >= 1 (got "${envRetryAttempts}")`));
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
retryAttemptsOverride = n;
|
|
927
|
+
}
|
|
928
|
+
if (retryAttemptsOverride !== undefined) {
|
|
929
|
+
resolvedRetry = { ...resolvedRetry, maxAttempts: retryAttemptsOverride };
|
|
930
|
+
}
|
|
931
|
+
if (resolvedRetry?.maxAttempts !== undefined && resolvedRetry.maxAttempts > 1) {
|
|
932
|
+
logProgress(TAG, `Retry enabled: up to ${resolvedRetry.maxAttempts} attempts per AI call`);
|
|
933
|
+
}
|
|
630
934
|
try {
|
|
631
|
-
const
|
|
935
|
+
const scanOptions = {
|
|
632
936
|
repositoryPath: effectiveRepoPath,
|
|
633
937
|
checks: checksWithDetails,
|
|
634
938
|
agentProvider: provider,
|
|
@@ -646,7 +950,13 @@ export async function runScan(args) {
|
|
|
646
950
|
// Prefer the provider's resolved auth mode (covers auto-detected local login);
|
|
647
951
|
// fall back to the env var for providers without the concept (e.g. mock).
|
|
648
952
|
isLocalClaude: provider?.isLocalMode?.() ?? (process.env.AGHAST_LOCAL_CLAUDE === 'true'),
|
|
649
|
-
|
|
953
|
+
judge: judgeOptions,
|
|
954
|
+
// Undefined when absent, which leaves the scan runner on its defaults —
|
|
955
|
+
// and the default is retry OFF, so a scan with no retry configuration
|
|
956
|
+
// anywhere fails fast exactly as it did before retry existed.
|
|
957
|
+
retry: resolvedRetry,
|
|
958
|
+
};
|
|
959
|
+
const outcome = await runMultiScanWithCost(scanOptions);
|
|
650
960
|
const results = outcome.results;
|
|
651
961
|
// Resolve output path: --output flag > runtime config dir > default
|
|
652
962
|
let resolvedOutputPath;
|
|
@@ -662,6 +972,17 @@ export async function runScan(args) {
|
|
|
662
972
|
}
|
|
663
973
|
await mkdir(dirname(resolvedOutputPath), { recursive: true });
|
|
664
974
|
await writeFile(resolvedOutputPath, formatter.format(results), 'utf-8');
|
|
975
|
+
// Optional: write one file per issue alongside the main report (Spec E.3.2).
|
|
976
|
+
let individualIssueDir;
|
|
977
|
+
let individualIssueCount = 0;
|
|
978
|
+
if (runtimeConfig.reporting?.includeIndividualIssueFiles && results.issues.length > 0) {
|
|
979
|
+
const issueFormat = runtimeConfig.reporting.individualIssueFormat ?? 'markdown';
|
|
980
|
+
const issueOutputDir = dirname(resolvedOutputPath);
|
|
981
|
+
const written = await writeIndividualIssueFiles(results, issueOutputDir, issueFormat);
|
|
982
|
+
individualIssueDir = written.rootDir;
|
|
983
|
+
individualIssueCount = written.files.length;
|
|
984
|
+
logProgress(TAG, `Wrote ${individualIssueCount} individual issue file(s) to ${individualIssueDir}`);
|
|
985
|
+
}
|
|
665
986
|
// Summary output
|
|
666
987
|
const statusIcon = results.summary.failedChecks > 0
|
|
667
988
|
? 'ISSUES DETECTED'
|
|
@@ -695,8 +1016,16 @@ export async function runScan(args) {
|
|
|
695
1016
|
const equiv = outcome.costCoveredBySubscription ? ' equivalent' : '';
|
|
696
1017
|
console.log(` Cost: $${outcome.totalCostUsd.toFixed(4)}${equiv} ${label}`);
|
|
697
1018
|
}
|
|
1019
|
+
if (outcome.judgeSummary) {
|
|
1020
|
+
const js = outcome.judgeSummary;
|
|
1021
|
+
const truePos = js.judgedIssues - js.falsePositives - js.uncertainJudgements;
|
|
1022
|
+
console.log(` Judged: ${js.judgedIssues} issues: ${truePos} true / ${js.falsePositives} false / ${js.uncertainJudgements} uncertain (judge: ${outcome.judgeModel})`);
|
|
1023
|
+
}
|
|
698
1024
|
console.log(` Duration: ${globalTimer.elapsedStr()}`);
|
|
699
1025
|
console.log(` Results: ${resolvedOutputPath}`);
|
|
1026
|
+
if (individualIssueDir) {
|
|
1027
|
+
console.log(` Issue files: ${individualIssueCount} in ${individualIssueDir}`);
|
|
1028
|
+
}
|
|
700
1029
|
console.log('='.repeat(60));
|
|
701
1030
|
// Persist the scan record to history (best-effort — never blocks exit).
|
|
702
1031
|
// We DO save the record even when the scan was aborted by budget: per-period
|
|
@@ -741,6 +1070,32 @@ export async function runScan(args) {
|
|
|
741
1070
|
console.error(formatError(ERROR_CODES.E7001, reason));
|
|
742
1071
|
logProgress(TAG, `Scan aborted by budget: ${reason}`);
|
|
743
1072
|
}
|
|
1073
|
+
// ─── Optional: post findings as PR comments ──────────────────────────────
|
|
1074
|
+
if (parsed.prNumber !== undefined) {
|
|
1075
|
+
const repoSlug = parsed.prRepo ?? process.env.GITHUB_REPOSITORY;
|
|
1076
|
+
const repoParts = parseRepoSlug(repoSlug);
|
|
1077
|
+
if (!repoParts) {
|
|
1078
|
+
console.error(formatError(ERROR_CODES.E1001, '--pr was supplied but --repo (or $GITHUB_REPOSITORY) is missing or not in owner/repo form'));
|
|
1079
|
+
await closeAllHandlers();
|
|
1080
|
+
process.exit(1);
|
|
1081
|
+
}
|
|
1082
|
+
try {
|
|
1083
|
+
const headSha = parsed.headSha ?? process.env.GITHUB_HEAD_SHA ?? process.env.GITHUB_SHA;
|
|
1084
|
+
const baseSha = parsed.baseSha ?? process.env.GITHUB_BASE_SHA;
|
|
1085
|
+
const summary = await postPRComments(results, {
|
|
1086
|
+
owner: repoParts.owner,
|
|
1087
|
+
repo: repoParts.repo,
|
|
1088
|
+
prNumber: parsed.prNumber,
|
|
1089
|
+
baseSha,
|
|
1090
|
+
headSha,
|
|
1091
|
+
});
|
|
1092
|
+
console.log(` PR comments: posted ${summary.posted}, skipped ${summary.skipped}`);
|
|
1093
|
+
}
|
|
1094
|
+
catch (err) {
|
|
1095
|
+
console.error(formatError(ERROR_CODES.E7201, `Failed to post PR comments: ${err instanceof Error ? err.message : String(err)}`));
|
|
1096
|
+
// Non-fatal: don't override scan exit code, but log a warning.
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
744
1099
|
// Exit code based on --fail-on-check-failure flag or runtime config (spec Section 9.3),
|
|
745
1100
|
// OR a budget abort.
|
|
746
1101
|
const failOnCheckFailure = parsed.failOnCheckFailure || runtimeConfig.failOnCheckFailure === true;
|