@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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared cost-tracking utilities for the scan runner and judge stage.
|
|
3
|
+
*/
|
|
4
|
+
import { type PricingConfig, type CostBreakdown } from './cost-calculator.js';
|
|
5
|
+
import { type BudgetLimits } from './budget.js';
|
|
6
|
+
import type { TokenUsage } from './types.js';
|
|
7
|
+
import type { ScanRecord } from './scan-history.js';
|
|
8
|
+
/**
|
|
9
|
+
* Tracks accumulated tokens/cost across a scan so the budget can be evaluated
|
|
10
|
+
* before each AI call. Mutated in place by AI invocations.
|
|
11
|
+
*/
|
|
12
|
+
export interface ScanCostTracker {
|
|
13
|
+
pricing?: PricingConfig;
|
|
14
|
+
budgetLimits?: BudgetLimits;
|
|
15
|
+
scanHistory?: ScanRecord[];
|
|
16
|
+
totalTokens: number;
|
|
17
|
+
totalCostUsd: number;
|
|
18
|
+
/** Cost source from the last recorded AI call. Used for banner labelling. */
|
|
19
|
+
lastCostSource?: CostBreakdown['source'];
|
|
20
|
+
lastCostReportedBy?: CostBreakdown['reportedBy'];
|
|
21
|
+
lastCostCoveredBySubscription?: boolean;
|
|
22
|
+
/** Set true after the first warn so we don't log it repeatedly. */
|
|
23
|
+
warned: boolean;
|
|
24
|
+
/** Most recent budget action returned to the runner. */
|
|
25
|
+
lastAction: 'continue' | 'warn' | 'abort';
|
|
26
|
+
/** Reason from the most recent non-continue check. */
|
|
27
|
+
lastReason?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function createCostTracker(options: {
|
|
30
|
+
pricing?: PricingConfig;
|
|
31
|
+
budgetLimits?: BudgetLimits;
|
|
32
|
+
scanHistory?: ScanRecord[];
|
|
33
|
+
}): ScanCostTracker;
|
|
34
|
+
/**
|
|
35
|
+
* Record an AI call's token usage against the tracker. Called after each
|
|
36
|
+
* successful executeCheck().
|
|
37
|
+
*/
|
|
38
|
+
export declare function recordUsage(tracker: ScanCostTracker, usage: TokenUsage | undefined, model: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* Check the budget before an AI call. Logs a warning the first time the warn
|
|
41
|
+
* threshold is crossed; throws BudgetExceededError when the abort threshold is
|
|
42
|
+
* crossed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function preflightBudget(tracker: ScanCostTracker): void;
|
|
45
|
+
//# sourceMappingURL=cost-tracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-tracker.d.ts","sourceRoot":"","sources":["../src/cost-tracker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAiB,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC7F,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAElF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAIpD;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACzC,kBAAkB,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACjD,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,mEAAmE;IACnE,MAAM,EAAE,OAAO,CAAC;IAChB,wDAAwD;IACxD,UAAU,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC;IAC1C,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IACzC,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B,GAAG,eAAe,CAUlB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,KAAK,EAAE,MAAM,GACZ,IAAI,CAQN;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAmB9D"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared cost-tracking utilities for the scan runner and judge stage.
|
|
3
|
+
*/
|
|
4
|
+
import { calculateCost } from './cost-calculator.js';
|
|
5
|
+
import { checkBudget, BudgetExceededError } from './budget.js';
|
|
6
|
+
import { logProgress } from './logging.js';
|
|
7
|
+
const TAG = 'scan';
|
|
8
|
+
export function createCostTracker(options) {
|
|
9
|
+
return {
|
|
10
|
+
pricing: options.pricing,
|
|
11
|
+
budgetLimits: options.budgetLimits,
|
|
12
|
+
scanHistory: options.scanHistory,
|
|
13
|
+
totalTokens: 0,
|
|
14
|
+
totalCostUsd: 0,
|
|
15
|
+
warned: false,
|
|
16
|
+
lastAction: 'continue',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Record an AI call's token usage against the tracker. Called after each
|
|
21
|
+
* successful executeCheck().
|
|
22
|
+
*/
|
|
23
|
+
export function recordUsage(tracker, usage, model) {
|
|
24
|
+
if (!usage || !tracker.pricing)
|
|
25
|
+
return;
|
|
26
|
+
const cost = calculateCost(usage, model, tracker.pricing);
|
|
27
|
+
tracker.totalTokens += usage.totalTokens;
|
|
28
|
+
tracker.totalCostUsd += cost.totalCost;
|
|
29
|
+
tracker.lastCostSource = cost.source;
|
|
30
|
+
tracker.lastCostReportedBy = cost.reportedBy;
|
|
31
|
+
tracker.lastCostCoveredBySubscription = cost.coveredBySubscription;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check the budget before an AI call. Logs a warning the first time the warn
|
|
35
|
+
* threshold is crossed; throws BudgetExceededError when the abort threshold is
|
|
36
|
+
* crossed.
|
|
37
|
+
*/
|
|
38
|
+
export function preflightBudget(tracker) {
|
|
39
|
+
if (!tracker.budgetLimits)
|
|
40
|
+
return;
|
|
41
|
+
const status = checkBudget({
|
|
42
|
+
currentScanCostUsd: tracker.totalCostUsd,
|
|
43
|
+
currentScanTokens: tracker.totalTokens,
|
|
44
|
+
history: tracker.scanHistory,
|
|
45
|
+
}, tracker.budgetLimits);
|
|
46
|
+
tracker.lastAction = status.action;
|
|
47
|
+
tracker.lastReason = status.reason;
|
|
48
|
+
if (status.action === 'abort') {
|
|
49
|
+
throw new BudgetExceededError(status.reason ?? 'Budget limit exceeded');
|
|
50
|
+
}
|
|
51
|
+
if (status.action === 'warn' && !tracker.warned) {
|
|
52
|
+
tracker.warned = true;
|
|
53
|
+
logProgress(TAG, `Budget warning: ${status.reason ?? 'approaching limit'}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=cost-tracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-tracker.js","sourceRoot":"","sources":["../src/cost-tracker.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAA0C,MAAM,sBAAsB,CAAC;AAC7F,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAqB,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAI3C,MAAM,GAAG,GAAG,MAAM,CAAC;AAwBnB,MAAM,UAAU,iBAAiB,CAAC,OAIjC;IACC,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,WAAW,EAAE,CAAC;QACd,YAAY,EAAE,CAAC;QACf,MAAM,EAAE,KAAK;QACb,UAAU,EAAE,UAAU;KACvB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,OAAwB,EACxB,KAA6B,EAC7B,KAAa;IAEb,IAAI,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,OAAO;QAAE,OAAO;IACvC,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,OAAO,CAAC,WAAW,IAAI,KAAK,CAAC,WAAW,CAAC;IACzC,OAAO,CAAC,YAAY,IAAI,IAAI,CAAC,SAAS,CAAC;IACvC,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC;IACrC,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,UAAU,CAAC;IAC7C,OAAO,CAAC,6BAA6B,GAAG,IAAI,CAAC,qBAAqB,CAAC;AACrE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,OAAwB;IACtD,IAAI,CAAC,OAAO,CAAC,YAAY;QAAE,OAAO;IAClC,MAAM,MAAM,GAAG,WAAW,CACxB;QACE,kBAAkB,EAAE,OAAO,CAAC,YAAY;QACxC,iBAAiB,EAAE,OAAO,CAAC,WAAW;QACtC,OAAO,EAAE,OAAO,CAAC,WAAW;KAC7B,EACD,OAAO,CAAC,YAAY,CACrB,CAAC;IACF,OAAO,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC;IACnC,OAAO,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC;IACnC,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QAC9B,MAAM,IAAI,mBAAmB,CAAC,MAAM,CAAC,MAAM,IAAI,uBAAuB,CAAC,CAAC;IAC1E,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QAChD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,CAAC,GAAG,EAAE,mBAAmB,MAAM,CAAC,MAAM,IAAI,mBAAmB,EAAE,CAAC,CAAC;IAC9E,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob-based target discovery.
|
|
3
|
+
*
|
|
4
|
+
* Matches files in the repository by glob pattern (e.g. "src/routes/**\/*.ts")
|
|
5
|
+
* and returns each matched file as a whole-file target. Useful for checks that
|
|
6
|
+
* apply to entire files (e.g. "review every GraphQL resolver"), without
|
|
7
|
+
* requiring an external tool such as Semgrep.
|
|
8
|
+
*
|
|
9
|
+
* Spec reference: Appendix E.2.1.
|
|
10
|
+
*/
|
|
11
|
+
import type { TargetDiscovery, DiscoveredTarget } from '../discovery.js';
|
|
12
|
+
/**
|
|
13
|
+
* Discover whole-file targets in `repoPath` matching `pattern`.
|
|
14
|
+
*
|
|
15
|
+
* Exposed as a named export (rather than only the registry-facing object) so
|
|
16
|
+
* unit tests can exercise the matching logic directly without going through
|
|
17
|
+
* the SecurityCheck plumbing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function discoverGlobTargets(pattern: string, repoPath: string, maxTargets?: number): Promise<DiscoveredTarget[]>;
|
|
20
|
+
export declare const globDiscovery: TargetDiscovery;
|
|
21
|
+
//# sourceMappingURL=glob-discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob-discovery.d.ts","sourceRoot":"","sources":["../../src/discoveries/glob-discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AASH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAoB,MAAM,iBAAiB,CAAC;AAmI3F;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAoD7B;AAED,eAAO,MAAM,aAAa,EAAE,eAoC3B,CAAC"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob-based target discovery.
|
|
3
|
+
*
|
|
4
|
+
* Matches files in the repository by glob pattern (e.g. "src/routes/**\/*.ts")
|
|
5
|
+
* and returns each matched file as a whole-file target. Useful for checks that
|
|
6
|
+
* apply to entire files (e.g. "review every GraphQL resolver"), without
|
|
7
|
+
* requiring an external tool such as Semgrep.
|
|
8
|
+
*
|
|
9
|
+
* Spec reference: Appendix E.2.1.
|
|
10
|
+
*/
|
|
11
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
12
|
+
import { createReadStream } from 'node:fs';
|
|
13
|
+
import { join, relative, sep } from 'node:path';
|
|
14
|
+
import picomatch from 'picomatch';
|
|
15
|
+
import { logDebug } from '../logging.js';
|
|
16
|
+
import { ERROR_CODES, formatError } from '../error-codes.js';
|
|
17
|
+
import { DEFAULT_GENERIC_PROMPT } from '../defaults.js';
|
|
18
|
+
const TAG = 'glob-discovery';
|
|
19
|
+
/**
|
|
20
|
+
* Skip files larger than this (in bytes) to avoid OOM when a wide glob
|
|
21
|
+
* (e.g. "**\/*") accidentally matches a large generated/binary file.
|
|
22
|
+
* Files above this threshold are excluded from the target set with a debug log.
|
|
23
|
+
*/
|
|
24
|
+
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MiB
|
|
25
|
+
/** Directories that are always skipped when walking the repository tree. */
|
|
26
|
+
const ALWAYS_SKIP_DIRS = new Set([
|
|
27
|
+
'.git',
|
|
28
|
+
'node_modules',
|
|
29
|
+
'.venv',
|
|
30
|
+
'venv',
|
|
31
|
+
'__pycache__',
|
|
32
|
+
'.tox',
|
|
33
|
+
'.mypy_cache',
|
|
34
|
+
'.pytest_cache',
|
|
35
|
+
'dist',
|
|
36
|
+
'build',
|
|
37
|
+
'.next',
|
|
38
|
+
'.nuxt',
|
|
39
|
+
'.cache',
|
|
40
|
+
'.idea',
|
|
41
|
+
'.vscode',
|
|
42
|
+
]);
|
|
43
|
+
function buildTargetPromptEnrichment(file, endLine) {
|
|
44
|
+
return `\n\nTARGET FILE:
|
|
45
|
+
|
|
46
|
+
You are analyzing a specific file:
|
|
47
|
+
- File: ${file}
|
|
48
|
+
- Lines: 1-${endLine} (whole file)
|
|
49
|
+
|
|
50
|
+
You MUST:
|
|
51
|
+
- Analyze ONLY this specific file — do not search for or report issues in other files
|
|
52
|
+
- You may read other files to understand context (e.g., imports, type definitions, data flow), but only report issues in this file
|
|
53
|
+
- Do NOT scan the broader repository for other instances of this vulnerability pattern
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Walk a directory tree recursively and yield every file path (POSIX-normalized,
|
|
58
|
+
* relative to `root`). Skips well-known noise directories so glob patterns do
|
|
59
|
+
* not have to opt out of `node_modules`/`.git` etc. on every check.
|
|
60
|
+
*
|
|
61
|
+
* Symlinks are NOT followed: `Dirent.isDirectory()` returns true only for real
|
|
62
|
+
* directories, so symlinked directories (and any cycles they could form) are
|
|
63
|
+
* skipped. Symlinked files are also skipped (`isFile()` returns false). This
|
|
64
|
+
* is the safe default — there's no general way to detect a hostile cycle, and
|
|
65
|
+
* source repos rarely rely on symlink-to-directory for layout.
|
|
66
|
+
*/
|
|
67
|
+
async function* walkFiles(root) {
|
|
68
|
+
async function* walk(dir) {
|
|
69
|
+
let entries;
|
|
70
|
+
try {
|
|
71
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const full = join(dir, entry.name);
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
if (ALWAYS_SKIP_DIRS.has(entry.name))
|
|
80
|
+
continue;
|
|
81
|
+
yield* walk(full);
|
|
82
|
+
}
|
|
83
|
+
else if (entry.isFile()) {
|
|
84
|
+
const rel = relative(root, full).split(sep).join('/');
|
|
85
|
+
yield rel;
|
|
86
|
+
}
|
|
87
|
+
// Symlinks (entry.isSymbolicLink()) are intentionally skipped — see
|
|
88
|
+
// the function-level comment above.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
yield* walk(root);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Count the number of lines in a file by streaming and counting `\n` bytes.
|
|
95
|
+
* Constant memory; safe for very large files.
|
|
96
|
+
*
|
|
97
|
+
* Returns 1 for empty files so that `endLine >= startLine` always holds.
|
|
98
|
+
* Returns `undefined` if the file cannot be read or exceeds `MAX_FILE_SIZE_BYTES`
|
|
99
|
+
* — callers should treat that as "skip this target" and log accordingly.
|
|
100
|
+
*/
|
|
101
|
+
async function countLines(filePath) {
|
|
102
|
+
let size;
|
|
103
|
+
try {
|
|
104
|
+
const st = await stat(filePath);
|
|
105
|
+
size = st.size;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
logDebug(TAG, `failed to stat ${filePath} for line count: ${err instanceof Error ? err.message : String(err)}`);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
if (size > MAX_FILE_SIZE_BYTES) {
|
|
112
|
+
logDebug(TAG, `skipping ${filePath}: size ${size} bytes exceeds limit ${MAX_FILE_SIZE_BYTES}`);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
if (size === 0)
|
|
116
|
+
return 1;
|
|
117
|
+
return new Promise((resolvePromise) => {
|
|
118
|
+
let newlineCount = 0;
|
|
119
|
+
let endsWithNewline = false;
|
|
120
|
+
const stream = createReadStream(filePath);
|
|
121
|
+
stream.on('data', (chunk) => {
|
|
122
|
+
// Treat the stream as bytes — counting `\n` bytes is safe for UTF-8 because
|
|
123
|
+
// 0x0A never appears as a continuation byte of a multi-byte sequence.
|
|
124
|
+
const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
125
|
+
for (let i = 0; i < buf.length; i++) {
|
|
126
|
+
if (buf[i] === 0x0a)
|
|
127
|
+
newlineCount++;
|
|
128
|
+
}
|
|
129
|
+
if (buf.length > 0)
|
|
130
|
+
endsWithNewline = buf[buf.length - 1] === 0x0a;
|
|
131
|
+
});
|
|
132
|
+
stream.on('error', (err) => {
|
|
133
|
+
logDebug(TAG, `failed to read ${filePath} for line count: ${err.message}`);
|
|
134
|
+
resolvePromise(undefined);
|
|
135
|
+
});
|
|
136
|
+
stream.on('end', () => {
|
|
137
|
+
// If the file ends with a newline, the count of \n equals the number of lines.
|
|
138
|
+
// If it does not, the trailing partial line adds one to the count.
|
|
139
|
+
const lines = endsWithNewline ? newlineCount : newlineCount + 1;
|
|
140
|
+
resolvePromise(lines || 1);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Discover whole-file targets in `repoPath` matching `pattern`.
|
|
146
|
+
*
|
|
147
|
+
* Exposed as a named export (rather than only the registry-facing object) so
|
|
148
|
+
* unit tests can exercise the matching logic directly without going through
|
|
149
|
+
* the SecurityCheck plumbing.
|
|
150
|
+
*/
|
|
151
|
+
export async function discoverGlobTargets(pattern, repoPath, maxTargets) {
|
|
152
|
+
if (!pattern || pattern.trim() === '') {
|
|
153
|
+
throw new Error(formatError(ERROR_CODES.E2004, 'Glob discovery requires a non-empty "glob" pattern'));
|
|
154
|
+
}
|
|
155
|
+
const matcher = picomatch(pattern, { dot: false });
|
|
156
|
+
const matches = [];
|
|
157
|
+
for await (const rel of walkFiles(repoPath)) {
|
|
158
|
+
if (matcher(rel)) {
|
|
159
|
+
matches.push(rel);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Sort BEFORE applying maxTargets so that the truncated subset is deterministic
|
|
163
|
+
// across platforms (readdir order varies by filesystem). Without this, a glob
|
|
164
|
+
// with maxTargets=N could analyze a different N files on Linux vs Windows vs
|
|
165
|
+
// macOS, making findings non-reproducible.
|
|
166
|
+
matches.sort();
|
|
167
|
+
const limited = typeof maxTargets === 'number' ? matches.slice(0, maxTargets) : matches;
|
|
168
|
+
// Compute line counts in parallel; drop unreadable / oversized files.
|
|
169
|
+
const lineCounts = await Promise.all(limited.map((rel) => countLines(join(repoPath, rel))));
|
|
170
|
+
const usable = [];
|
|
171
|
+
for (let i = 0; i < limited.length; i++) {
|
|
172
|
+
const endLine = lineCounts[i];
|
|
173
|
+
if (endLine === undefined)
|
|
174
|
+
continue; // unreadable / oversized — already logged
|
|
175
|
+
usable.push({ rel: limited[i], endLine });
|
|
176
|
+
}
|
|
177
|
+
// Surface a single summary log when files were dropped, so users debugging
|
|
178
|
+
// "why is my big bundle missing from the target set?" don't have to enable
|
|
179
|
+
// trace-level logging to find the per-file skip messages from countLines.
|
|
180
|
+
const skippedCount = limited.length - usable.length;
|
|
181
|
+
if (skippedCount > 0) {
|
|
182
|
+
logDebug(TAG, `${skippedCount} matched file${skippedCount === 1 ? '' : 's'} skipped (unreadable or > ${MAX_FILE_SIZE_BYTES} bytes)`);
|
|
183
|
+
}
|
|
184
|
+
return usable.map(({ rel, endLine }, idx) => ({
|
|
185
|
+
file: rel,
|
|
186
|
+
startLine: 1,
|
|
187
|
+
endLine,
|
|
188
|
+
label: `[file ${idx + 1}/${usable.length}]`,
|
|
189
|
+
promptEnrichment: buildTargetPromptEnrichment(rel, endLine),
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
export const globDiscovery = {
|
|
193
|
+
name: 'glob',
|
|
194
|
+
defaultGenericPrompt: DEFAULT_GENERIC_PROMPT,
|
|
195
|
+
needsInstructions: true,
|
|
196
|
+
// Opted out deliberately. `supportsDiffFilter` became a required field on
|
|
197
|
+
// TargetDiscovery after this discovery was written (#227), and the diff
|
|
198
|
+
// filter is built around findings with meaningful line ranges. Glob targets
|
|
199
|
+
// are whole files, so filtering them has never been designed or tested for
|
|
200
|
+
// this shape. Opting out preserves the behaviour this discovery was built
|
|
201
|
+
// and tested with: a glob check scans every matching file. Enabling it later
|
|
202
|
+
// is a small change plus tests for the whole-file case.
|
|
203
|
+
supportsDiffFilter: false,
|
|
204
|
+
async discover(check, repoPath, _options) {
|
|
205
|
+
const checkTarget = check.checkTarget;
|
|
206
|
+
if (!checkTarget.glob) {
|
|
207
|
+
throw new Error(formatError(ERROR_CODES.E2004, `Check "${check.id}" uses glob discovery but has no "glob" pattern in its check definition`));
|
|
208
|
+
}
|
|
209
|
+
logDebug(TAG, `Running glob discovery for check "${check.id}": ${checkTarget.glob}`);
|
|
210
|
+
const targets = await discoverGlobTargets(checkTarget.glob, repoPath, checkTarget.maxTargets);
|
|
211
|
+
logDebug(TAG, `Discovered ${targets.length} files matching pattern`);
|
|
212
|
+
return targets;
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
//# sourceMappingURL=glob-discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob-discovery.js","sourceRoot":"","sources":["../../src/discoveries/glob-discovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script-based target discovery.
|
|
3
|
+
*
|
|
4
|
+
* Runs a user-provided script (`node` or `bash`) in the target repository
|
|
5
|
+
* and parses its stdout to produce discovery targets. Useful when target
|
|
6
|
+
* discovery is too custom for Semgrep / SARIF / OpenAnt — e.g. parsing an
|
|
7
|
+
* OpenAPI spec, querying a build manifest, walking a database schema, etc.
|
|
8
|
+
*
|
|
9
|
+
* ─── Trust model ─────────────────────────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* Scripts run with the full privileges of the aghast process. They are not
|
|
12
|
+
* sandboxed. Treat any script referenced by a check definition as trusted
|
|
13
|
+
* code — only enable script-discovery checks whose script you (or your team)
|
|
14
|
+
* have read and audited.
|
|
15
|
+
*
|
|
16
|
+
* Defensive measures applied here:
|
|
17
|
+
* - Script path must resolve INSIDE the configured check folder or repo
|
|
18
|
+
* (no `..` escape, no absolute paths to /etc/shadow, etc.).
|
|
19
|
+
* - Symlink-aware: both the script path and `cwd` are realpath'd and
|
|
20
|
+
* re-checked against the repo root, so a symlink inside the check folder
|
|
21
|
+
* pointing outside the repo cannot be used to execute external code.
|
|
22
|
+
* - Spawned with `shell: false` and array args — no shell interpolation, no
|
|
23
|
+
* command injection from check-definition fields.
|
|
24
|
+
* - Spawned with a curated environment: only a small allow-list of neutral
|
|
25
|
+
* env vars (PATH, HOME, locale, temp dirs, Windows essentials) is forwarded.
|
|
26
|
+
* As defence-in-depth, any var matching a known-secret pattern (`*_KEY`,
|
|
27
|
+
* `*_TOKEN`, `*_SECRET`, `*_PASSWORD`, or `ANTHROPIC_*` / `OPENAI_*` /
|
|
28
|
+
* `GITHUB_*` / `AGHAST_*` / `AWS_*` / `GCP_*` / `AZURE_*` prefixes) is
|
|
29
|
+
* also explicitly dropped. Allow-list matching is case-insensitive so
|
|
30
|
+
* the Windows-style `Path` / `SystemRoot` casing works the same as POSIX
|
|
31
|
+
* `PATH`.
|
|
32
|
+
* - cwd, when supplied, must resolve (and realpath) inside the repo root.
|
|
33
|
+
* - Hard timeout (default 30s, configurable per check).
|
|
34
|
+
* - Stdout is bounded (8 MiB) to prevent memory exhaustion from runaway scripts.
|
|
35
|
+
* - Both the line-format and JSON-format output paths cap the number of
|
|
36
|
+
* parsed targets (MAX_LINE_TARGETS / MAX_JSON_TARGETS) to bound memory
|
|
37
|
+
* even if a script floods within the stdout cap.
|
|
38
|
+
* - Output is parsed as data (lines / JSON) — never `eval`-ed.
|
|
39
|
+
* - File paths returned by the script are validated to be relative & inside
|
|
40
|
+
* the repo (no `..` escape, no absolute paths) before they are forwarded
|
|
41
|
+
* to the AI prompt or snippet extractor.
|
|
42
|
+
* - On `bash` scripts, Windows is unsupported (no system bash) and a clear
|
|
43
|
+
* error is raised rather than silently falling back.
|
|
44
|
+
*/
|
|
45
|
+
import type { TargetDiscovery } from '../discovery.js';
|
|
46
|
+
/**
|
|
47
|
+
* Build a curated environment for the spawned discovery script.
|
|
48
|
+
*
|
|
49
|
+
* Discovery scripts need to walk the repo, possibly run small helper tools,
|
|
50
|
+
* but they never need API credentials. We start from a minimal allow-list of
|
|
51
|
+
* "neutral" env vars (PATH/locale/temp dirs) and then ALSO drop any var whose
|
|
52
|
+
* name matches a known-secret pattern even if it slipped through. Both layers
|
|
53
|
+
* are defence-in-depth.
|
|
54
|
+
*
|
|
55
|
+
* Exported for testing.
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildScriptEnv(parent?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
58
|
+
interface ScriptTargetInput {
|
|
59
|
+
file: string;
|
|
60
|
+
startLine?: number;
|
|
61
|
+
endLine?: number;
|
|
62
|
+
message?: string;
|
|
63
|
+
snippet?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* True when `candidateAbs` lives inside `rootAbs`. Both must be absolute.
|
|
67
|
+
*
|
|
68
|
+
* Two ways to be outside, and both matter:
|
|
69
|
+
* - a `..` prefix, the ordinary case;
|
|
70
|
+
* - an absolute result, which is what `relative()` returns on Windows when
|
|
71
|
+
* the two paths are on different drives (`C:\repo` vs `D:\checks`).
|
|
72
|
+
*
|
|
73
|
+
* Exported so the rule can be unit-tested against Windows-style paths on any
|
|
74
|
+
* platform. It exists as a single function because it previously did not: two
|
|
75
|
+
* call sites implemented the rule separately, one omitted the absolute-path
|
|
76
|
+
* case, and cross-drive layouts were rejected as a result.
|
|
77
|
+
*
|
|
78
|
+
* Pass a `pathApi` of `path.win32` or `path.posix` to test either flavour.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isInsideRoot(rootAbs: string, candidateAbs: string, pathApi?: {
|
|
81
|
+
relative: (a: string, b: string) => string;
|
|
82
|
+
isAbsolute: (p: string) => boolean;
|
|
83
|
+
}): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Parse stdout into a list of discovery target inputs.
|
|
86
|
+
* - `lines`: one file path per non-empty, non-comment line.
|
|
87
|
+
* - `json-array`: a JSON array of strings (file paths) or objects.
|
|
88
|
+
* - `json-object`: an object with a `targets` array of strings or objects.
|
|
89
|
+
*
|
|
90
|
+
* Both `lines` and JSON formats cap parsed targets at MAX_LINE_TARGETS /
|
|
91
|
+
* MAX_JSON_TARGETS (100k each); the rest are dropped with a warning log.
|
|
92
|
+
*
|
|
93
|
+
* Output is treated as DATA: never eval'd, never interpolated into commands.
|
|
94
|
+
*/
|
|
95
|
+
export declare function parseScriptOutput(raw: string, format: 'lines' | 'json-array' | 'json-object'): ScriptTargetInput[];
|
|
96
|
+
interface RunScriptResult {
|
|
97
|
+
stdout: string;
|
|
98
|
+
exitCode: number | null;
|
|
99
|
+
signal: NodeJS.Signals | null;
|
|
100
|
+
stderr: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Execute the script as a child process and capture stdout.
|
|
104
|
+
* - Uses `spawn` with `shell: false`: never invoke a shell, never interpolate.
|
|
105
|
+
* - Passes a curated env (no API keys / tokens) — see `buildScriptEnv`.
|
|
106
|
+
* - Bounds stdout/stderr buffers and kills the process on timeout.
|
|
107
|
+
*/
|
|
108
|
+
export declare function runScript(opts: {
|
|
109
|
+
command: string;
|
|
110
|
+
args: string[];
|
|
111
|
+
cwd: string;
|
|
112
|
+
timeoutMs: number;
|
|
113
|
+
env?: NodeJS.ProcessEnv;
|
|
114
|
+
}): Promise<RunScriptResult>;
|
|
115
|
+
export declare const scriptDiscovery: TargetDiscovery;
|
|
116
|
+
export {};
|
|
117
|
+
//# sourceMappingURL=script-discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"script-discovery.d.ts","sourceRoot":"","sources":["../../src/discoveries/script-discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAQH,OAAO,KAAK,EAAE,eAAe,EAAsC,MAAM,iBAAiB,CAAC;AAoB3F;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,MAAM,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAAC,UAAU,CA8DzF;AAED,UAAU,iBAAiB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAAC,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;CAC/D,GACzB,OAAO,CAGT;AA4GD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,GAAG,YAAY,GAAG,aAAa,GAC7C,iBAAiB,EAAE,CAsIrB;AAED,UAAU,eAAe;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,GAAG,OAAO,CAAC,eAAe,CAAC,CAsF3B;AAgCD,eAAO,MAAM,eAAe,EAAE,eAyM7B,CAAC"}
|