@bouncesecurity/aghast 0.5.0 → 0.6.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/README.md +8 -3
- package/config/pricing.json +42 -0
- package/config/prompts/false-positive-validation.md +1 -0
- package/config/prompts/general-vuln-discovery.md +1 -0
- package/config/prompts/generic-instructions.md +3 -2
- package/dist/budget.d.ts +62 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +137 -0
- package/dist/budget.js.map +1 -0
- package/dist/check-library.d.ts +1 -0
- package/dist/check-library.d.ts.map +1 -1
- package/dist/check-library.js +22 -6
- package/dist/check-library.js.map +1 -1
- package/dist/claude-code-provider.d.ts +0 -2
- package/dist/claude-code-provider.d.ts.map +1 -1
- package/dist/claude-code-provider.js +66 -16
- package/dist/claude-code-provider.js.map +1 -1
- package/dist/cli.js +11 -1
- package/dist/cli.js.map +1 -1
- package/dist/cost-calculator.d.ts +80 -0
- package/dist/cost-calculator.d.ts.map +1 -0
- package/dist/cost-calculator.js +226 -0
- package/dist/cost-calculator.js.map +1 -0
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +2 -0
- package/dist/error-codes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -10
- package/dist/index.js.map +1 -1
- package/dist/logging.d.ts +1 -1
- package/dist/logging.d.ts.map +1 -1
- package/dist/logging.js +25 -33
- package/dist/logging.js.map +1 -1
- package/dist/mock-agent-provider.d.ts +6 -3
- package/dist/mock-agent-provider.d.ts.map +1 -1
- package/dist/mock-agent-provider.js +12 -5
- package/dist/mock-agent-provider.js.map +1 -1
- package/dist/opencode-provider.d.ts +0 -2
- package/dist/opencode-provider.d.ts.map +1 -1
- package/dist/opencode-provider.js +90 -17
- package/dist/opencode-provider.js.map +1 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +92 -0
- package/dist/runtime-config.js.map +1 -1
- package/dist/scan-history.d.ts +82 -0
- package/dist/scan-history.d.ts.map +1 -0
- package/dist/scan-history.js +127 -0
- package/dist/scan-history.js.map +1 -0
- package/dist/scan-runner.d.ts +64 -1
- package/dist/scan-runner.d.ts.map +1 -1
- package/dist/scan-runner.js +173 -14
- package/dist/scan-runner.js.map +1 -1
- package/dist/stats.d.ts +11 -0
- package/dist/stats.d.ts.map +1 -0
- package/dist/stats.js +197 -0
- package/dist/stats.js.map +1 -0
- package/dist/types.d.ts +40 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan history: persisted record of completed scans for cost dashboards
|
|
3
|
+
* and budget controls.
|
|
4
|
+
*
|
|
5
|
+
* Storage: `~/.aghast/history.json` by default (one file per user). When the
|
|
6
|
+
* home directory cannot be resolved, falls back to project-local
|
|
7
|
+
* `.aghast-history.json`. The path can be overridden in tests via the
|
|
8
|
+
* `AGHAST_HISTORY_FILE` env var or by passing `historyFile` to the helpers.
|
|
9
|
+
*
|
|
10
|
+
* Format: a single JSON document `{ "records": [...] }`. We keep this simple
|
|
11
|
+
* (no SQLite) so the file is human-readable and can be edited / pruned by
|
|
12
|
+
* hand. Corrupt files are logged and rebuilt to avoid blocking scans on a
|
|
13
|
+
* malformed history file.
|
|
14
|
+
*/
|
|
15
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { resolve, dirname } from 'node:path';
|
|
18
|
+
import { logProgress } from './logging.js';
|
|
19
|
+
const TAG = 'scan-history';
|
|
20
|
+
const DEFAULT_FILENAME = 'history.json';
|
|
21
|
+
const FALLBACK_FILENAME = '.aghast-history.json';
|
|
22
|
+
const SCHEMA_VERSION = 1;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the history file path.
|
|
25
|
+
*
|
|
26
|
+
* Precedence:
|
|
27
|
+
* 1. explicit `historyFile` argument
|
|
28
|
+
* 2. AGHAST_HISTORY_FILE env var
|
|
29
|
+
* 3. ~/.aghast/history.json when homedir is available
|
|
30
|
+
* 4. project-local `.aghast-history.json`
|
|
31
|
+
*/
|
|
32
|
+
export function resolveHistoryFilePath(historyFile) {
|
|
33
|
+
if (historyFile)
|
|
34
|
+
return resolve(historyFile);
|
|
35
|
+
const envOverride = process.env.AGHAST_HISTORY_FILE;
|
|
36
|
+
if (envOverride && envOverride.length > 0)
|
|
37
|
+
return resolve(envOverride);
|
|
38
|
+
let home;
|
|
39
|
+
try {
|
|
40
|
+
home = homedir();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
home = undefined;
|
|
44
|
+
}
|
|
45
|
+
if (home && home.length > 0) {
|
|
46
|
+
return resolve(home, '.aghast', DEFAULT_FILENAME);
|
|
47
|
+
}
|
|
48
|
+
return resolve(process.cwd(), FALLBACK_FILENAME);
|
|
49
|
+
}
|
|
50
|
+
async function readHistoryFile(path) {
|
|
51
|
+
let content;
|
|
52
|
+
try {
|
|
53
|
+
content = await readFile(path, 'utf-8');
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (err.code === 'ENOENT') {
|
|
57
|
+
return { version: SCHEMA_VERSION, records: [] };
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(content);
|
|
63
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
64
|
+
throw new Error('not an object');
|
|
65
|
+
}
|
|
66
|
+
const obj = parsed;
|
|
67
|
+
const records = Array.isArray(obj.records) ? obj.records : [];
|
|
68
|
+
const version = typeof obj.version === 'number' ? obj.version : SCHEMA_VERSION;
|
|
69
|
+
return { version, records };
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// Corrupt history: log and rebuild, never block a scan.
|
|
73
|
+
logProgress(TAG, `History file at ${path} is corrupt (${err instanceof Error ? err.message : String(err)}); rebuilding.`);
|
|
74
|
+
return { version: SCHEMA_VERSION, records: [] };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function writeHistoryFile(path, file) {
|
|
78
|
+
await mkdir(dirname(path), { recursive: true });
|
|
79
|
+
await writeFile(path, JSON.stringify(file, null, 2) + '\n', 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Append a record to the scan history, deduplicating by scanId.
|
|
83
|
+
* If a record with the same scanId already exists, it is replaced.
|
|
84
|
+
*/
|
|
85
|
+
export async function saveScanRecord(record, options = {}) {
|
|
86
|
+
const path = resolveHistoryFilePath(options.historyFile);
|
|
87
|
+
const file = await readHistoryFile(path);
|
|
88
|
+
const existingIdx = file.records.findIndex((r) => r.scanId === record.scanId);
|
|
89
|
+
if (existingIdx >= 0) {
|
|
90
|
+
file.records[existingIdx] = record;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
file.records.push(record);
|
|
94
|
+
}
|
|
95
|
+
await writeHistoryFile(path, file);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Load all scan records, applying optional filters.
|
|
99
|
+
* Records are returned newest-first (descending startedAt).
|
|
100
|
+
*/
|
|
101
|
+
export async function queryScanHistory(filters = {}, options = {}) {
|
|
102
|
+
const path = resolveHistoryFilePath(options.historyFile);
|
|
103
|
+
const file = await readHistoryFile(path);
|
|
104
|
+
let out = file.records.slice();
|
|
105
|
+
if (filters.repository) {
|
|
106
|
+
const needle = filters.repository;
|
|
107
|
+
out = out.filter((r) => r.repository === needle ||
|
|
108
|
+
r.repositoryUrl === needle ||
|
|
109
|
+
r.repository.includes(needle) ||
|
|
110
|
+
(r.repositoryUrl ?? '').includes(needle));
|
|
111
|
+
}
|
|
112
|
+
if (filters.model) {
|
|
113
|
+
const needle = filters.model;
|
|
114
|
+
out = out.filter((r) => r.models.some((m) => m.includes(needle)));
|
|
115
|
+
}
|
|
116
|
+
if (filters.since) {
|
|
117
|
+
const since = filters.since;
|
|
118
|
+
out = out.filter((r) => r.startedAt >= since);
|
|
119
|
+
}
|
|
120
|
+
if (filters.until) {
|
|
121
|
+
const until = filters.until;
|
|
122
|
+
out = out.filter((r) => r.startedAt <= until);
|
|
123
|
+
}
|
|
124
|
+
out.sort((a, b) => (a.startedAt < b.startedAt ? 1 : a.startedAt > b.startedAt ? -1 : 0));
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=scan-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan-history.js","sourceRoot":"","sources":["../src/scan-history.ts"],"names":[],"mappings":"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"}
|
package/dist/scan-runner.d.ts
CHANGED
|
@@ -3,7 +3,19 @@
|
|
|
3
3
|
* Runs security checks against a repository and produces ScanResults.
|
|
4
4
|
* Implements the core workflow from spec Section 2.2.
|
|
5
5
|
*/
|
|
6
|
-
import { type AgentProvider, type RepositoryInfo, type CheckDetails, type SecurityCheck, type ScanResults } from './types.js';
|
|
6
|
+
import { type AgentProvider, type RepositoryInfo, type CheckDetails, type SecurityCheck, type ScanResults, type TokenUsage } from './types.js';
|
|
7
|
+
import { type PricingConfig, type CostBreakdown } from './cost-calculator.js';
|
|
8
|
+
import { type BudgetLimits } from './budget.js';
|
|
9
|
+
import type { ScanRecord } from './scan-history.js';
|
|
10
|
+
/**
|
|
11
|
+
* Sum multiple TokenUsage values into one aggregate.
|
|
12
|
+
* Returns undefined if no inputs have token usage.
|
|
13
|
+
*
|
|
14
|
+
* reportedCost is aggregated only when every contributing call has it — a
|
|
15
|
+
* single missing cost means we cannot produce an accurate total, so we fall
|
|
16
|
+
* back to undefined (which triggers rate-based estimation later).
|
|
17
|
+
*/
|
|
18
|
+
export declare function sumTokenUsage(usages: (TokenUsage | undefined)[]): TokenUsage | undefined;
|
|
7
19
|
export interface MultiScanOptions {
|
|
8
20
|
repositoryPath: string;
|
|
9
21
|
checks: Array<{
|
|
@@ -17,13 +29,64 @@ export interface MultiScanOptions {
|
|
|
17
29
|
repositoryInfo?: RepositoryInfo;
|
|
18
30
|
configDir?: string;
|
|
19
31
|
genericPrompt?: string;
|
|
32
|
+
/** Pricing config for cost calculations. */
|
|
33
|
+
pricing?: PricingConfig;
|
|
34
|
+
/** Optional budget limits enforced before each AI call. */
|
|
35
|
+
budgetLimits?: BudgetLimits;
|
|
36
|
+
/** Pre-loaded scan history (for period budget checks). */
|
|
37
|
+
scanHistory?: ScanRecord[];
|
|
38
|
+
/** true when AGHAST_LOCAL_CLAUDE=true — triggers budget warning if limits are also set */
|
|
39
|
+
isLocalClaude?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Tracks accumulated tokens/cost across a scan so the budget can be evaluated
|
|
43
|
+
* before each AI call. Mutated in place by AI invocations.
|
|
44
|
+
*/
|
|
45
|
+
export interface ScanCostTracker {
|
|
46
|
+
pricing?: PricingConfig;
|
|
47
|
+
budgetLimits?: BudgetLimits;
|
|
48
|
+
scanHistory?: ScanRecord[];
|
|
49
|
+
totalTokens: number;
|
|
50
|
+
totalCostUsd: number;
|
|
51
|
+
/** Cost source from the last recorded AI call. Used for banner labelling. */
|
|
52
|
+
lastCostSource?: CostBreakdown['source'];
|
|
53
|
+
lastCostReportedBy?: CostBreakdown['reportedBy'];
|
|
54
|
+
lastCostCoveredBySubscription?: boolean;
|
|
55
|
+
/** Set true after the first warn so we don't log it repeatedly. */
|
|
56
|
+
warned: boolean;
|
|
57
|
+
/** Most recent budget action returned to the runner. */
|
|
58
|
+
lastAction: 'continue' | 'warn' | 'abort';
|
|
59
|
+
/** Reason from the most recent non-continue check. */
|
|
60
|
+
lastReason?: string;
|
|
20
61
|
}
|
|
21
62
|
/**
|
|
22
63
|
* Generate a scanId in the format: scan-<timestamp>-<hash>
|
|
23
64
|
*/
|
|
24
65
|
export declare function generateScanId(): string;
|
|
66
|
+
/** Aggregated ScanResults plus computed cost summary. */
|
|
67
|
+
export interface MultiScanOutcome {
|
|
68
|
+
results: ScanResults;
|
|
69
|
+
totalCostUsd: number;
|
|
70
|
+
currency: string;
|
|
71
|
+
models: string[];
|
|
72
|
+
/** How the cost was determined (for banner/stats labelling). */
|
|
73
|
+
costSource?: CostBreakdown['source'];
|
|
74
|
+
/** Which provider reported the cost when costSource === 'reported'. */
|
|
75
|
+
costReportedBy?: CostBreakdown['reportedBy'];
|
|
76
|
+
/** true when AGHAST_LOCAL_CLAUDE=true — amount is API-equivalent, not billed */
|
|
77
|
+
costCoveredBySubscription?: boolean;
|
|
78
|
+
/** True when the scan was halted by a budget abort. */
|
|
79
|
+
budgetAborted?: boolean;
|
|
80
|
+
/** Reason from the budget abort, when budgetAborted is true. */
|
|
81
|
+
budgetAbortReason?: string;
|
|
82
|
+
}
|
|
25
83
|
/**
|
|
26
84
|
* Run multiple security checks and return aggregated ScanResults.
|
|
27
85
|
*/
|
|
28
86
|
export declare function runMultiScan(options: MultiScanOptions): Promise<ScanResults>;
|
|
87
|
+
/**
|
|
88
|
+
* Same as runMultiScan but also returns the computed cost summary.
|
|
89
|
+
* Used by the CLI to record scan history.
|
|
90
|
+
*/
|
|
91
|
+
export declare function runMultiScanWithCost(options: MultiScanOptions): Promise<MultiScanOutcome>;
|
|
29
92
|
//# sourceMappingURL=scan-runner.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan-runner.d.ts","sourceRoot":"","sources":["../src/scan-runner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAkBH,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,cAAc,EAInB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,WAAW,
|
|
1
|
+
{"version":3,"file":"scan-runner.d.ts","sourceRoot":"","sources":["../src/scan-runner.ts"],"names":[],"mappings":"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"}
|
package/dist/scan-runner.js
CHANGED
|
@@ -11,7 +11,7 @@ import { buildPrompt } from './prompt-template.js';
|
|
|
11
11
|
import { parseAgentResponse } from './response-parser.js';
|
|
12
12
|
import { extractSnippet } from './snippet-extractor.js';
|
|
13
13
|
import { analyzeRepository } from './repository-analyzer.js';
|
|
14
|
-
import { logProgress, logDebug, createTimer } from './logging.js';
|
|
14
|
+
import { logProgress, logDebug, logWarn, createTimer } from './logging.js';
|
|
15
15
|
import { CHECK_TYPE } from './check-types.js';
|
|
16
16
|
import { getDiscovery, registerDiscovery } from './discovery.js';
|
|
17
17
|
import { DEFAULT_PROVIDER_NAME } from './provider-registry.js';
|
|
@@ -19,9 +19,14 @@ import { semgrepDiscovery } from './discoveries/semgrep-discovery.js';
|
|
|
19
19
|
import { openantDiscovery } from './discoveries/openant-discovery.js';
|
|
20
20
|
import { sarifDiscovery } from './discoveries/sarif-discovery.js';
|
|
21
21
|
import { DEFAULT_MODEL, FatalProviderError, } from './types.js';
|
|
22
|
+
import { calculateCost } from './cost-calculator.js';
|
|
23
|
+
import { checkBudget, BudgetExceededError } from './budget.js';
|
|
22
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
25
|
const TAG = 'scan';
|
|
24
26
|
const DEFAULT_CONCURRENCY = 5;
|
|
27
|
+
// Per-target AI call timeout. Without this, a single hung provider request stalls
|
|
28
|
+
// the worker indefinitely; with concurrency=N hangs, the entire check freezes.
|
|
29
|
+
const DEFAULT_TARGET_TIMEOUT_MS = 5 * 60 * 1000;
|
|
25
30
|
// --- Register built-in discovery implementations ---
|
|
26
31
|
registerDiscovery(semgrepDiscovery);
|
|
27
32
|
registerDiscovery(openantDiscovery);
|
|
@@ -29,15 +34,34 @@ registerDiscovery(sarifDiscovery);
|
|
|
29
34
|
/**
|
|
30
35
|
* Sum multiple TokenUsage values into one aggregate.
|
|
31
36
|
* Returns undefined if no inputs have token usage.
|
|
37
|
+
*
|
|
38
|
+
* reportedCost is aggregated only when every contributing call has it — a
|
|
39
|
+
* single missing cost means we cannot produce an accurate total, so we fall
|
|
40
|
+
* back to undefined (which triggers rate-based estimation later).
|
|
32
41
|
*/
|
|
33
|
-
function sumTokenUsage(usages) {
|
|
42
|
+
export function sumTokenUsage(usages) {
|
|
34
43
|
const defined = usages.filter((u) => u !== undefined);
|
|
35
44
|
if (defined.length === 0)
|
|
36
45
|
return undefined;
|
|
46
|
+
// Optional fields: sum when at least one is present; preserve undefined when all absent.
|
|
47
|
+
const sumOptional = (getter) => {
|
|
48
|
+
const values = defined.map(getter).filter((v) => v !== undefined);
|
|
49
|
+
return values.length > 0 ? values.reduce((a, b) => a + b, 0) : undefined;
|
|
50
|
+
};
|
|
51
|
+
// reportedCost: only aggregate when every item has it (uniform provider, single source).
|
|
52
|
+
let reportedCost;
|
|
53
|
+
if (defined.every((u) => u.reportedCost !== undefined)) {
|
|
54
|
+
const total = defined.reduce((sum, u) => sum + u.reportedCost.amountUsd, 0);
|
|
55
|
+
reportedCost = { amountUsd: total, source: defined[0].reportedCost.source };
|
|
56
|
+
}
|
|
37
57
|
return {
|
|
38
58
|
inputTokens: defined.reduce((sum, u) => sum + u.inputTokens, 0),
|
|
39
59
|
outputTokens: defined.reduce((sum, u) => sum + u.outputTokens, 0),
|
|
60
|
+
cacheCreationInputTokens: sumOptional((u) => u.cacheCreationInputTokens),
|
|
61
|
+
cacheReadInputTokens: sumOptional((u) => u.cacheReadInputTokens),
|
|
62
|
+
reasoningTokens: sumOptional((u) => u.reasoningTokens),
|
|
40
63
|
totalTokens: defined.reduce((sum, u) => sum + u.totalTokens, 0),
|
|
64
|
+
...(reportedCost !== undefined ? { reportedCost } : {}),
|
|
41
65
|
};
|
|
42
66
|
}
|
|
43
67
|
/**
|
|
@@ -88,6 +112,54 @@ async function mapWithConcurrency(items, concurrency, fn, abortHandle) {
|
|
|
88
112
|
}
|
|
89
113
|
return results;
|
|
90
114
|
}
|
|
115
|
+
function createCostTracker(options) {
|
|
116
|
+
return {
|
|
117
|
+
pricing: options.pricing,
|
|
118
|
+
budgetLimits: options.budgetLimits,
|
|
119
|
+
scanHistory: options.scanHistory,
|
|
120
|
+
totalTokens: 0,
|
|
121
|
+
totalCostUsd: 0,
|
|
122
|
+
warned: false,
|
|
123
|
+
lastAction: 'continue',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Record an AI call's token usage against the tracker. Called after each
|
|
128
|
+
* successful executeCheck().
|
|
129
|
+
*/
|
|
130
|
+
function recordUsage(tracker, usage, model) {
|
|
131
|
+
if (!usage || !tracker.pricing)
|
|
132
|
+
return;
|
|
133
|
+
const cost = calculateCost(usage, model, tracker.pricing);
|
|
134
|
+
tracker.totalTokens += usage.totalTokens;
|
|
135
|
+
tracker.totalCostUsd += cost.totalCost;
|
|
136
|
+
tracker.lastCostSource = cost.source;
|
|
137
|
+
tracker.lastCostReportedBy = cost.reportedBy;
|
|
138
|
+
tracker.lastCostCoveredBySubscription = cost.coveredBySubscription;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Check the budget before an AI call. Logs a warning the first time the warn
|
|
142
|
+
* threshold is crossed; throws BudgetExceededError when the abort threshold is
|
|
143
|
+
* crossed.
|
|
144
|
+
*/
|
|
145
|
+
function preflightBudget(tracker) {
|
|
146
|
+
if (!tracker.budgetLimits)
|
|
147
|
+
return;
|
|
148
|
+
const status = checkBudget({
|
|
149
|
+
currentScanCostUsd: tracker.totalCostUsd,
|
|
150
|
+
currentScanTokens: tracker.totalTokens,
|
|
151
|
+
history: tracker.scanHistory,
|
|
152
|
+
}, tracker.budgetLimits);
|
|
153
|
+
tracker.lastAction = status.action;
|
|
154
|
+
tracker.lastReason = status.reason;
|
|
155
|
+
if (status.action === 'abort') {
|
|
156
|
+
throw new BudgetExceededError(status.reason ?? 'Budget limit exceeded');
|
|
157
|
+
}
|
|
158
|
+
if (status.action === 'warn' && !tracker.warned) {
|
|
159
|
+
tracker.warned = true;
|
|
160
|
+
logProgress(TAG, `Budget warning: ${status.reason ?? 'approaching limit'}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
91
163
|
/**
|
|
92
164
|
* Generate a scanId in the format: scan-<timestamp>-<hash>
|
|
93
165
|
*/
|
|
@@ -223,14 +295,14 @@ async function mapTargetToIssue(target, checkId, checkName, repositoryPath, chec
|
|
|
223
295
|
* Execute a single check against a repository.
|
|
224
296
|
* Routes to the appropriate execution path based on check type.
|
|
225
297
|
*/
|
|
226
|
-
async function executeSingleCheck(check, checkName, checkInstructions, repositoryPath, agentProvider, checkMetadata, concurrency, configDir, genericPrompt) {
|
|
298
|
+
async function executeSingleCheck(check, checkName, checkInstructions, repositoryPath, agentProvider, costTracker, checkMetadata, concurrency, configDir, genericPrompt) {
|
|
227
299
|
const checkId = check.id;
|
|
228
300
|
// Route to targeted execution (discovery + AI analysis)
|
|
229
301
|
if (check.checkTarget?.type === CHECK_TYPE.TARGETED) {
|
|
230
302
|
if (!agentProvider) {
|
|
231
303
|
throw new Error(`Check "${checkId}" requires an agent provider but none was configured`);
|
|
232
304
|
}
|
|
233
|
-
return executeTargetedCheck(check, checkName, checkInstructions, repositoryPath, agentProvider, checkMetadata, concurrency, configDir, genericPrompt);
|
|
305
|
+
return executeTargetedCheck(check, checkName, checkInstructions, repositoryPath, agentProvider, costTracker, checkMetadata, concurrency, configDir, genericPrompt);
|
|
234
306
|
}
|
|
235
307
|
// Route to static execution (discovery + direct mapping, no AI)
|
|
236
308
|
if (check.checkTarget?.type === CHECK_TYPE.STATIC) {
|
|
@@ -247,7 +319,10 @@ async function executeSingleCheck(check, checkName, checkInstructions, repositor
|
|
|
247
319
|
let summary;
|
|
248
320
|
const checkTimer = createTimer();
|
|
249
321
|
try {
|
|
322
|
+
preflightBudget(costTracker);
|
|
250
323
|
const agentResponse = await agentProvider.executeCheck(prompt, repositoryPath);
|
|
324
|
+
const model = agentProvider.getModelName?.() ?? DEFAULT_MODEL;
|
|
325
|
+
recordUsage(costTracker, agentResponse.tokenUsage, model);
|
|
251
326
|
const executionTime = checkTimer.elapsed();
|
|
252
327
|
logDebug(TAG, `Agent response: ${agentResponse.raw.length} chars`);
|
|
253
328
|
const parsed = agentResponse.parsed ?? parseAgentResponse(agentResponse.raw);
|
|
@@ -300,8 +375,8 @@ async function executeSingleCheck(check, checkName, checkInstructions, repositor
|
|
|
300
375
|
}
|
|
301
376
|
}
|
|
302
377
|
catch (err) {
|
|
303
|
-
// Fatal errors must propagate up to
|
|
304
|
-
if (err instanceof FatalProviderError) {
|
|
378
|
+
// Fatal errors and budget aborts must propagate up to stop the scan
|
|
379
|
+
if (err instanceof FatalProviderError || err instanceof BudgetExceededError) {
|
|
305
380
|
throw err;
|
|
306
381
|
}
|
|
307
382
|
const executionTime = checkTimer.elapsed();
|
|
@@ -323,7 +398,7 @@ async function executeSingleCheck(check, checkName, checkInstructions, repositor
|
|
|
323
398
|
* The execution pipeline is generic — all discovery-specific behavior is
|
|
324
399
|
* encapsulated in the DiscoveredTarget data from the discovery implementation.
|
|
325
400
|
*/
|
|
326
|
-
async function executeTargetedCheck(check, checkName, checkInstructions, repositoryPath, agentProvider, checkMetadata, optionsConcurrency, configDir, genericPromptOverride) {
|
|
401
|
+
async function executeTargetedCheck(check, checkName, checkInstructions, repositoryPath, agentProvider, costTracker, checkMetadata, optionsConcurrency, configDir, genericPromptOverride) {
|
|
327
402
|
const checkId = check.id;
|
|
328
403
|
const checkTarget = check.checkTarget;
|
|
329
404
|
const discoveryName = checkTarget.discovery;
|
|
@@ -387,9 +462,31 @@ async function executeTargetedCheck(check, checkName, checkInstructions, reposit
|
|
|
387
462
|
targetResults = await mapWithConcurrency(targets, effectiveConcurrency, async (target, _idx) => {
|
|
388
463
|
inProgressCount++;
|
|
389
464
|
try {
|
|
465
|
+
try {
|
|
466
|
+
preflightBudget(costTracker);
|
|
467
|
+
}
|
|
468
|
+
catch (budgetErr) {
|
|
469
|
+
if (budgetErr instanceof BudgetExceededError) {
|
|
470
|
+
abortHandle.aborted = true;
|
|
471
|
+
abortHandle.reason = budgetErr;
|
|
472
|
+
throw budgetErr;
|
|
473
|
+
}
|
|
474
|
+
throw budgetErr;
|
|
475
|
+
}
|
|
390
476
|
const prompt = basePrompt + (target.promptEnrichment ?? '');
|
|
391
477
|
logDebug(TAG, `${target.label} Analyzing: ${target.file}:${target.startLine}-${target.endLine}`);
|
|
392
|
-
|
|
478
|
+
let timeoutHandle;
|
|
479
|
+
const agentResponse = await Promise.race([
|
|
480
|
+
agentProvider.executeCheck(prompt, repositoryPath, target.label, target.agentOptions),
|
|
481
|
+
new Promise((_, reject) => {
|
|
482
|
+
timeoutHandle = setTimeout(() => reject(new Error(`Agent provider timed out after ${DEFAULT_TARGET_TIMEOUT_MS / 1000}s on target ${target.label}`)), DEFAULT_TARGET_TIMEOUT_MS);
|
|
483
|
+
}),
|
|
484
|
+
]).finally(() => {
|
|
485
|
+
if (timeoutHandle)
|
|
486
|
+
clearTimeout(timeoutHandle);
|
|
487
|
+
});
|
|
488
|
+
const model = agentProvider.getModelName?.() ?? DEFAULT_MODEL;
|
|
489
|
+
recordUsage(costTracker, agentResponse.tokenUsage, model);
|
|
393
490
|
const parsed = agentResponse.parsed ?? parseAgentResponse(agentResponse.raw);
|
|
394
491
|
if (!parsed) {
|
|
395
492
|
logDebug(TAG, `${target.label} Returned malformed response`);
|
|
@@ -409,8 +506,8 @@ async function executeTargetedCheck(check, checkName, checkInstructions, reposit
|
|
|
409
506
|
return { issues, error: false, flagged: parsed.flagged === true, tokenUsage: agentResponse.tokenUsage };
|
|
410
507
|
}
|
|
411
508
|
catch (err) {
|
|
412
|
-
// Fatal errors: signal abort and re-throw to stop other workers
|
|
413
|
-
if (err instanceof FatalProviderError) {
|
|
509
|
+
// Fatal errors and budget aborts: signal abort and re-throw to stop other workers
|
|
510
|
+
if (err instanceof FatalProviderError || err instanceof BudgetExceededError) {
|
|
414
511
|
abortHandle.aborted = true;
|
|
415
512
|
abortHandle.reason = err;
|
|
416
513
|
throw err;
|
|
@@ -472,8 +569,8 @@ async function executeTargetedCheck(check, checkName, checkInstructions, reposit
|
|
|
472
569
|
};
|
|
473
570
|
}
|
|
474
571
|
catch (err) {
|
|
475
|
-
// Fatal errors must propagate up to
|
|
476
|
-
if (err instanceof FatalProviderError) {
|
|
572
|
+
// Fatal errors and budget aborts must propagate up to stop the scan
|
|
573
|
+
if (err instanceof FatalProviderError || err instanceof BudgetExceededError) {
|
|
477
574
|
throw err;
|
|
478
575
|
}
|
|
479
576
|
const executionTime = checkTimer.elapsed();
|
|
@@ -564,6 +661,14 @@ async function executeStaticCheck(check, checkName, repositoryPath, checkMetadat
|
|
|
564
661
|
* Run multiple security checks and return aggregated ScanResults.
|
|
565
662
|
*/
|
|
566
663
|
export async function runMultiScan(options) {
|
|
664
|
+
const outcome = await runMultiScanWithCost(options);
|
|
665
|
+
return outcome.results;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Same as runMultiScan but also returns the computed cost summary.
|
|
669
|
+
* Used by the CLI to record scan history.
|
|
670
|
+
*/
|
|
671
|
+
export async function runMultiScanWithCost(options) {
|
|
567
672
|
const { repositoryPath, checks, agentProvider, modelName, agentProviderName, concurrency, configDir, genericPrompt } = options;
|
|
568
673
|
const scanTimer = createTimer();
|
|
569
674
|
const scanId = generateScanId();
|
|
@@ -571,6 +676,9 @@ export async function runMultiScan(options) {
|
|
|
571
676
|
const version = await getVersion();
|
|
572
677
|
logProgress(TAG, `Starting scan ${scanId} (${checks.length} ${checks.length === 1 ? 'check' : 'checks'})`);
|
|
573
678
|
logDebug(TAG, `Repository: ${repositoryPath}`);
|
|
679
|
+
if (options.isLocalClaude && options.budgetLimits) {
|
|
680
|
+
logWarn(TAG, 'Budget limits in local-Claude mode apply to equivalent API cost, not subscription usage.');
|
|
681
|
+
}
|
|
574
682
|
// Use pre-analyzed repository info if provided, otherwise analyze here
|
|
575
683
|
let repositoryInfo;
|
|
576
684
|
if (options.repositoryInfo) {
|
|
@@ -582,6 +690,10 @@ export async function runMultiScan(options) {
|
|
|
582
690
|
}
|
|
583
691
|
const allCheckSummaries = [];
|
|
584
692
|
const allIssues = [];
|
|
693
|
+
let budgetAborted = false;
|
|
694
|
+
let budgetAbortReason;
|
|
695
|
+
// Cost / budget tracking spans all checks in the scan.
|
|
696
|
+
const costTracker = createCostTracker(options);
|
|
585
697
|
// Track all models used during the scan
|
|
586
698
|
const modelsUsed = new Set();
|
|
587
699
|
if (modelName)
|
|
@@ -598,11 +710,38 @@ export async function runMultiScan(options) {
|
|
|
598
710
|
if (check.model)
|
|
599
711
|
modelsUsed.add(check.model);
|
|
600
712
|
try {
|
|
601
|
-
const { summary: checkSummary, issues } = await executeSingleCheck(check, details.name, details.content, repositoryPath, agentProvider, checkMetadata, concurrency, configDir, genericPrompt);
|
|
713
|
+
const { summary: checkSummary, issues } = await executeSingleCheck(check, details.name, details.content, repositoryPath, agentProvider, costTracker, checkMetadata, concurrency, configDir, genericPrompt);
|
|
602
714
|
allCheckSummaries.push(checkSummary);
|
|
603
715
|
allIssues.push(...issues);
|
|
604
716
|
}
|
|
605
717
|
catch (err) {
|
|
718
|
+
if (err instanceof BudgetExceededError) {
|
|
719
|
+
budgetAborted = true;
|
|
720
|
+
budgetAbortReason = err.message;
|
|
721
|
+
logProgress(TAG, `Budget exceeded during check "${check.id}": ${err.message}`);
|
|
722
|
+
allCheckSummaries.push({
|
|
723
|
+
checkId: check.id,
|
|
724
|
+
checkName: details.name,
|
|
725
|
+
status: 'ERROR',
|
|
726
|
+
issuesFound: 0,
|
|
727
|
+
executionTime: 0,
|
|
728
|
+
error: `Budget exceeded: ${err.message}`,
|
|
729
|
+
});
|
|
730
|
+
for (let ri = ci + 1; ri < checks.length; ri++) {
|
|
731
|
+
const remaining = checks[ri];
|
|
732
|
+
logProgress(TAG, `Skipping check "${remaining.check.id}" due to budget abort`);
|
|
733
|
+
allCheckSummaries.push({
|
|
734
|
+
checkId: remaining.check.id,
|
|
735
|
+
checkName: remaining.details.name,
|
|
736
|
+
status: 'ERROR',
|
|
737
|
+
issuesFound: 0,
|
|
738
|
+
executionTime: 0,
|
|
739
|
+
error: `Scan aborted: budget limit exceeded`,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
restoreModel(agentProvider, previousModel);
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
606
745
|
if (err instanceof FatalProviderError) {
|
|
607
746
|
// Record the failing check as ERROR
|
|
608
747
|
logProgress(TAG, `Fatal error during check "${check.id}": ${err.message}`);
|
|
@@ -668,6 +807,26 @@ export async function runMultiScan(options) {
|
|
|
668
807
|
: { name: 'none', models: [] },
|
|
669
808
|
tokenUsage: aggregateTokenUsage,
|
|
670
809
|
};
|
|
671
|
-
|
|
810
|
+
// Attach cost metadata when pricing was provided.
|
|
811
|
+
if (options.pricing) {
|
|
812
|
+
results.metadata = {
|
|
813
|
+
...(results.metadata ?? {}),
|
|
814
|
+
cost: {
|
|
815
|
+
totalCostUsd: costTracker.totalCostUsd,
|
|
816
|
+
currency: options.pricing.currency ?? 'USD',
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
results,
|
|
822
|
+
totalCostUsd: costTracker.totalCostUsd,
|
|
823
|
+
currency: options.pricing?.currency ?? 'USD',
|
|
824
|
+
models: [...modelsUsed],
|
|
825
|
+
costSource: costTracker.lastCostSource,
|
|
826
|
+
costReportedBy: costTracker.lastCostReportedBy,
|
|
827
|
+
costCoveredBySubscription: costTracker.lastCostCoveredBySubscription,
|
|
828
|
+
budgetAborted,
|
|
829
|
+
budgetAbortReason,
|
|
830
|
+
};
|
|
672
831
|
}
|
|
673
832
|
//# sourceMappingURL=scan-runner.js.map
|