@owasp-aghast/aghast 0.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +127 -0
  3. package/assets/img/aghastowaspcaption.png +0 -0
  4. package/assets/txt/logo.txt +52 -0
  5. package/config/pricing.json +42 -0
  6. package/config/prompts/false-positive-validation.md +78 -0
  7. package/config/prompts/general-vuln-discovery.md +99 -0
  8. package/config/prompts/generic-instructions.md +59 -0
  9. package/config/prompts/test-cheaper-instructions.md +57 -0
  10. package/dist/budget.d.ts +62 -0
  11. package/dist/budget.d.ts.map +1 -0
  12. package/dist/budget.js +137 -0
  13. package/dist/budget.js.map +1 -0
  14. package/dist/build-config.d.ts +15 -0
  15. package/dist/build-config.d.ts.map +1 -0
  16. package/dist/build-config.js +568 -0
  17. package/dist/build-config.js.map +1 -0
  18. package/dist/check-library.d.ts +88 -0
  19. package/dist/check-library.d.ts.map +1 -0
  20. package/dist/check-library.js +461 -0
  21. package/dist/check-library.js.map +1 -0
  22. package/dist/check-types.d.ts +35 -0
  23. package/dist/check-types.d.ts.map +1 -0
  24. package/dist/check-types.js +66 -0
  25. package/dist/check-types.js.map +1 -0
  26. package/dist/claude-code-provider.d.ts +29 -0
  27. package/dist/claude-code-provider.d.ts.map +1 -0
  28. package/dist/claude-code-provider.js +347 -0
  29. package/dist/claude-code-provider.js.map +1 -0
  30. package/dist/cli.d.ts +13 -0
  31. package/dist/cli.d.ts.map +1 -0
  32. package/dist/cli.js +109 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/colors.d.ts +7 -0
  35. package/dist/colors.d.ts.map +1 -0
  36. package/dist/colors.js +18 -0
  37. package/dist/colors.js.map +1 -0
  38. package/dist/cost-calculator.d.ts +80 -0
  39. package/dist/cost-calculator.d.ts.map +1 -0
  40. package/dist/cost-calculator.js +226 -0
  41. package/dist/cost-calculator.js.map +1 -0
  42. package/dist/defaults.d.ts +21 -0
  43. package/dist/defaults.d.ts.map +1 -0
  44. package/dist/defaults.js +21 -0
  45. package/dist/defaults.js.map +1 -0
  46. package/dist/diff-filter.d.ts +50 -0
  47. package/dist/diff-filter.d.ts.map +1 -0
  48. package/dist/diff-filter.js +130 -0
  49. package/dist/diff-filter.js.map +1 -0
  50. package/dist/diff-parser.d.ts +40 -0
  51. package/dist/diff-parser.d.ts.map +1 -0
  52. package/dist/diff-parser.js +112 -0
  53. package/dist/diff-parser.js.map +1 -0
  54. package/dist/diff-unit-matcher.d.ts +53 -0
  55. package/dist/diff-unit-matcher.d.ts.map +1 -0
  56. package/dist/diff-unit-matcher.js +156 -0
  57. package/dist/diff-unit-matcher.js.map +1 -0
  58. package/dist/discoveries/openant-discovery.d.ts +10 -0
  59. package/dist/discoveries/openant-discovery.d.ts.map +1 -0
  60. package/dist/discoveries/openant-discovery.js +56 -0
  61. package/dist/discoveries/openant-discovery.js.map +1 -0
  62. package/dist/discoveries/sarif-discovery.d.ts +9 -0
  63. package/dist/discoveries/sarif-discovery.d.ts.map +1 -0
  64. package/dist/discoveries/sarif-discovery.js +56 -0
  65. package/dist/discoveries/sarif-discovery.js.map +1 -0
  66. package/dist/discoveries/semgrep-discovery.d.ts +9 -0
  67. package/dist/discoveries/semgrep-discovery.d.ts.map +1 -0
  68. package/dist/discoveries/semgrep-discovery.js +61 -0
  69. package/dist/discoveries/semgrep-discovery.js.map +1 -0
  70. package/dist/discovery.d.ts +100 -0
  71. package/dist/discovery.d.ts.map +1 -0
  72. package/dist/discovery.js +49 -0
  73. package/dist/discovery.js.map +1 -0
  74. package/dist/error-codes.d.ts +46 -0
  75. package/dist/error-codes.d.ts.map +1 -0
  76. package/dist/error-codes.js +70 -0
  77. package/dist/error-codes.js.map +1 -0
  78. package/dist/formatters/index.d.ts +10 -0
  79. package/dist/formatters/index.d.ts.map +1 -0
  80. package/dist/formatters/index.js +23 -0
  81. package/dist/formatters/index.js.map +1 -0
  82. package/dist/formatters/json-formatter.d.ts +11 -0
  83. package/dist/formatters/json-formatter.d.ts.map +1 -0
  84. package/dist/formatters/json-formatter.js +11 -0
  85. package/dist/formatters/json-formatter.js.map +1 -0
  86. package/dist/formatters/sarif-formatter.d.ts +18 -0
  87. package/dist/formatters/sarif-formatter.d.ts.map +1 -0
  88. package/dist/formatters/sarif-formatter.js +103 -0
  89. package/dist/formatters/sarif-formatter.js.map +1 -0
  90. package/dist/formatters/types.d.ts +11 -0
  91. package/dist/formatters/types.d.ts.map +1 -0
  92. package/dist/formatters/types.js +6 -0
  93. package/dist/formatters/types.js.map +1 -0
  94. package/dist/index.d.ts +7 -0
  95. package/dist/index.d.ts.map +1 -0
  96. package/dist/index.js +754 -0
  97. package/dist/index.js.map +1 -0
  98. package/dist/logging.d.ts +140 -0
  99. package/dist/logging.d.ts.map +1 -0
  100. package/dist/logging.js +344 -0
  101. package/dist/logging.js.map +1 -0
  102. package/dist/mock-agent-provider.d.ts +24 -0
  103. package/dist/mock-agent-provider.d.ts.map +1 -0
  104. package/dist/mock-agent-provider.js +38 -0
  105. package/dist/mock-agent-provider.js.map +1 -0
  106. package/dist/mock-ai-provider.d.ts +21 -0
  107. package/dist/mock-ai-provider.d.ts.map +1 -0
  108. package/dist/mock-ai-provider.js +31 -0
  109. package/dist/mock-ai-provider.js.map +1 -0
  110. package/dist/new-check.d.ts +13 -0
  111. package/dist/new-check.d.ts.map +1 -0
  112. package/dist/new-check.js +515 -0
  113. package/dist/new-check.js.map +1 -0
  114. package/dist/openant-loader.d.ts +105 -0
  115. package/dist/openant-loader.d.ts.map +1 -0
  116. package/dist/openant-loader.js +135 -0
  117. package/dist/openant-loader.js.map +1 -0
  118. package/dist/openant-runner.d.ts +30 -0
  119. package/dist/openant-runner.d.ts.map +1 -0
  120. package/dist/openant-runner.js +113 -0
  121. package/dist/openant-runner.js.map +1 -0
  122. package/dist/opencode-provider.d.ts +63 -0
  123. package/dist/opencode-provider.d.ts.map +1 -0
  124. package/dist/opencode-provider.js +719 -0
  125. package/dist/opencode-provider.js.map +1 -0
  126. package/dist/prompt-template.d.ts +12 -0
  127. package/dist/prompt-template.d.ts.map +1 -0
  128. package/dist/prompt-template.js +36 -0
  129. package/dist/prompt-template.js.map +1 -0
  130. package/dist/provider-registry.d.ts +15 -0
  131. package/dist/provider-registry.d.ts.map +1 -0
  132. package/dist/provider-registry.js +29 -0
  133. package/dist/provider-registry.js.map +1 -0
  134. package/dist/provider-utils.d.ts +52 -0
  135. package/dist/provider-utils.d.ts.map +1 -0
  136. package/dist/provider-utils.js +40 -0
  137. package/dist/provider-utils.js.map +1 -0
  138. package/dist/repository-analyzer.d.ts +68 -0
  139. package/dist/repository-analyzer.d.ts.map +1 -0
  140. package/dist/repository-analyzer.js +238 -0
  141. package/dist/repository-analyzer.js.map +1 -0
  142. package/dist/response-parser.d.ts +14 -0
  143. package/dist/response-parser.d.ts.map +1 -0
  144. package/dist/response-parser.js +111 -0
  145. package/dist/response-parser.js.map +1 -0
  146. package/dist/runtime-config.d.ts +15 -0
  147. package/dist/runtime-config.d.ts.map +1 -0
  148. package/dist/runtime-config.js +187 -0
  149. package/dist/runtime-config.js.map +1 -0
  150. package/dist/sarif-parser.d.ts +20 -0
  151. package/dist/sarif-parser.d.ts.map +1 -0
  152. package/dist/sarif-parser.js +76 -0
  153. package/dist/sarif-parser.js.map +1 -0
  154. package/dist/scan-history.d.ts +82 -0
  155. package/dist/scan-history.d.ts.map +1 -0
  156. package/dist/scan-history.js +127 -0
  157. package/dist/scan-history.js.map +1 -0
  158. package/dist/scan-runner.d.ts +122 -0
  159. package/dist/scan-runner.d.ts.map +1 -0
  160. package/dist/scan-runner.js +928 -0
  161. package/dist/scan-runner.js.map +1 -0
  162. package/dist/semgrep-runner.d.ts +25 -0
  163. package/dist/semgrep-runner.d.ts.map +1 -0
  164. package/dist/semgrep-runner.js +100 -0
  165. package/dist/semgrep-runner.js.map +1 -0
  166. package/dist/snippet-extractor.d.ts +25 -0
  167. package/dist/snippet-extractor.d.ts.map +1 -0
  168. package/dist/snippet-extractor.js +56 -0
  169. package/dist/snippet-extractor.js.map +1 -0
  170. package/dist/stats.d.ts +11 -0
  171. package/dist/stats.d.ts.map +1 -0
  172. package/dist/stats.js +197 -0
  173. package/dist/stats.js.map +1 -0
  174. package/dist/types.d.ts +321 -0
  175. package/dist/types.d.ts.map +1 -0
  176. package/dist/types.js +19 -0
  177. package/dist/types.js.map +1 -0
  178. package/package.json +61 -0
package/dist/index.js ADDED
@@ -0,0 +1,754 @@
1
+ /**
2
+ * CLI entry point for aghast.
3
+ * Usage: aghast scan <repository-path> --config-dir <path> [options]
4
+ */
5
+ import 'dotenv/config';
6
+ import { readFile, writeFile, stat, mkdir, readdir } from 'node:fs/promises';
7
+ import { resolve, dirname } from 'node:path';
8
+ import { runMultiScanWithCost } from './scan-runner.js';
9
+ import { loadDefaultPricing, mergePricing, formatCostSourceLabel } from './cost-calculator.js';
10
+ import { saveScanRecord, queryScanHistory } from './scan-history.js';
11
+ import { createProviderByName, getProviderNames, DEFAULT_PROVIDER_NAME } from './provider-registry.js';
12
+ import { loadCheckRegistry, discoverCheckFolders, resolveChecks, filterChecksForRepository, validateCheck, loadCheckDetails, } from './check-library.js';
13
+ import { analyzeRepository } from './repository-analyzer.js';
14
+ import { loadRuntimeConfig } from './runtime-config.js';
15
+ import { logProgress, logDebug, setLogLevel, createTimer, isValidLogLevel, initFileHandler, closeAllHandlers, getAvailableLogTypes } from './logging.js';
16
+ import { MOCK_MODEL_NAME, DEFAULT_MODEL } from './types.js';
17
+ import { getFormatter } from './formatters/index.js';
18
+ import { verifySemgrepInstalled } from './semgrep-runner.js';
19
+ import { verifyOpenAntInstalled } from './openant-runner.js';
20
+ import { MockAgentProvider } from './mock-agent-provider.js';
21
+ import { ERROR_CODES, formatError, formatFatalError } from './error-codes.js';
22
+ import { DEFAULT_OUTPUT_FORMAT, DEFAULT_LOG_LEVEL, DEFAULT_LOG_TYPE } from './defaults.js';
23
+ import { colorStatus } from './colors.js';
24
+ import { getCheckType } from './check-types.js';
25
+ import { getDiscovery, getRegisteredDiscoveries } from './discovery.js';
26
+ import { createRequire } from 'node:module';
27
+ const TAG = 'aghast';
28
+ async function createMockProvider() {
29
+ // AGHAST_MOCK_AI='true' → default empty response; AGHAST_MOCK_AI=<path> → read from that file
30
+ const mockAiValue = process.env.AGHAST_MOCK_AI;
31
+ let rawResponse = '{"issues": []}';
32
+ if (mockAiValue && mockAiValue !== 'true') {
33
+ try {
34
+ rawResponse = await readFile(resolve(mockAiValue), 'utf-8');
35
+ }
36
+ catch (err) {
37
+ throw new Error(`Failed to read AGHAST_MOCK_AI response file: ${mockAiValue}`, { cause: err });
38
+ }
39
+ }
40
+ // Optional mock token usage for testing cost/budget pipelines.
41
+ // Format: AGHAST_MOCK_TOKENS="<input>,<output>" (e.g. "1000,500")
42
+ // When AGHAST_LOCAL_CLAUDE=true, inject a mock reportedCost so that the
43
+ // coveredBySubscription path (banner "equivalent", label) is exercisable in tests.
44
+ let tokenUsage;
45
+ const mockTokensRaw = process.env.AGHAST_MOCK_TOKENS;
46
+ if (mockTokensRaw) {
47
+ const parts = mockTokensRaw.split(',').map((s) => Number(s.trim()));
48
+ if (parts.length === 2 && parts.every((n) => Number.isFinite(n) && n >= 0)) {
49
+ const useLocalClaude = process.env.AGHAST_LOCAL_CLAUDE === 'true';
50
+ tokenUsage = {
51
+ inputTokens: parts[0],
52
+ outputTokens: parts[1],
53
+ totalTokens: parts[0] + parts[1],
54
+ ...(useLocalClaude ? {
55
+ reportedCost: {
56
+ amountUsd: (parts[0] + parts[1]) / 1_000_000,
57
+ source: 'claude-agent-sdk',
58
+ coveredBySubscription: true,
59
+ },
60
+ } : {}),
61
+ };
62
+ }
63
+ }
64
+ const provider = new MockAgentProvider({ rawResponse, tokenUsage });
65
+ await provider.initialize({});
66
+ return provider;
67
+ }
68
+ const SCAN_HELP = `Usage: aghast scan <repo-path> --config-dir <path> [options]
69
+
70
+ Run security checks against a repository.
71
+
72
+ Arguments:
73
+ <repo-path> Path to the repository to scan
74
+
75
+ General options:
76
+ --config-dir <path> Config directory containing checks-config.json,
77
+ checks/ folder, and optionally runtime-config.json.
78
+ Required unless AGHAST_CONFIG_DIR is set.
79
+ --output <path> Output file path for results
80
+ (default: <repo-path>/security_checks_results.<ext>)
81
+ --output-format json|sarif Output format (default: json)
82
+ --fail-on-check-failure Exit with code 1 if any check FAILs or ERRORs
83
+ --debug Shorthand for --log-level debug
84
+ --log-level <level> Console log level: error, warn, info, debug, trace
85
+ (default: info)
86
+ --log-file <path> Write all log output to a file (always at trace level
87
+ unless overridden by --log-type)
88
+ --log-type <type> Log file handler type (default: file).
89
+ Available types: file
90
+ --model <model> AI model override (e.g. claude-sonnet-4-20250514)
91
+ --agent-provider <name> Agent provider name (default: claude-code)
92
+ --generic-prompt <file> Generic prompt template filename in prompts/ dir
93
+ --diff-ref <ref> Git ref to diff against (e.g. HEAD~1, main, SHA).
94
+ Auto-activates diff filtering on every check whose
95
+ discovery supports it, unless the check opts out
96
+ via checkTarget.diffFilter: false.
97
+ --diff-file <path> Path to pre-generated unified diff file
98
+ (alternative to --diff-ref)
99
+ --budget-limit-cost <usd> Abort the scan when accumulated cost exceeds this
100
+ USD value. Warns at 80%, aborts at 100%
101
+ --budget-limit-tokens <n> Abort the scan when accumulated tokens exceed n.
102
+ Warns at 80%, aborts at 100%
103
+
104
+ Environment variables:
105
+ ANTHROPIC_API_KEY API key for Claude (required for AI-based checks)
106
+ AGHAST_CONFIG_DIR Default config directory (CLI --config-dir takes precedence)
107
+ AGHAST_AI_MODEL AI model override (CLI --model takes precedence)
108
+ AGHAST_GENERIC_PROMPT Generic prompt template filename (CLI --generic-prompt takes precedence)
109
+ AGHAST_DEBUG Set to "true" to enable debug output (same as --debug)
110
+ AGHAST_LOG_LEVEL Console log level (CLI --log-level takes precedence)
111
+ AGHAST_LOG_FILE Log file path (CLI --log-file takes precedence)
112
+ AGHAST_LOG_TYPE Log file handler type (CLI --log-type takes precedence)
113
+ AGHAST_DIFF_REF Git ref to diff against (CLI --diff-ref takes precedence)
114
+
115
+ Examples:
116
+ aghast scan ./my-repo --config-dir ./my-checks
117
+ aghast scan ./my-repo --config-dir ./my-checks --output results.sarif --output-format sarif
118
+ aghast scan ./my-repo --config-dir ./my-checks --fail-on-check-failure --debug
119
+ aghast scan ./my-repo --config-dir ./my-checks --log-file scan.log --log-level warn
120
+ aghast scan ./my-repo --config-dir ./my-checks --model claude-sonnet-4-20250514`;
121
+ function parseArgs(args) {
122
+ if (args.length < 1 || args[0] === '--help' || args[0] === '-h') {
123
+ console.log(SCAN_HELP);
124
+ process.exit(0);
125
+ }
126
+ // First positional arg is repo-path
127
+ const firstArg = args[0];
128
+ const repositoryPath = firstArg && !firstArg.startsWith('--') ? resolve(firstArg) : undefined;
129
+ const startIdx = repositoryPath ? 1 : 0;
130
+ let configDir;
131
+ let outputFormat;
132
+ let outputPath;
133
+ const failOnCheckFailure = args.includes('--fail-on-check-failure');
134
+ const debug = args.includes('--debug');
135
+ let logLevel;
136
+ let logFile;
137
+ let logType;
138
+ let runtimeConfigPath;
139
+ let model;
140
+ let agentProvider;
141
+ let genericPrompt;
142
+ let diffRef;
143
+ let diffFile;
144
+ let budgetLimitCost;
145
+ let budgetLimitTokens;
146
+ for (let i = startIdx; i < args.length; i++) {
147
+ switch (args[i]) {
148
+ case '--config-dir': {
149
+ configDir = args[i + 1];
150
+ if (!configDir) {
151
+ console.error(formatError(ERROR_CODES.E1001, '--config-dir requires a path argument'));
152
+ process.exit(1);
153
+ }
154
+ configDir = resolve(configDir);
155
+ i++;
156
+ break;
157
+ }
158
+ case '--output': {
159
+ outputPath = args[i + 1];
160
+ if (!outputPath) {
161
+ console.error(formatError(ERROR_CODES.E1001, '--output requires a path argument'));
162
+ process.exit(1);
163
+ }
164
+ outputPath = resolve(outputPath);
165
+ i++;
166
+ break;
167
+ }
168
+ case '--output-format': {
169
+ outputFormat = args[i + 1];
170
+ if (!outputFormat) {
171
+ console.error(formatError(ERROR_CODES.E1001, '--output-format requires a format argument'));
172
+ process.exit(1);
173
+ }
174
+ i++;
175
+ break;
176
+ }
177
+ case '--runtime-config': {
178
+ runtimeConfigPath = args[i + 1];
179
+ if (!runtimeConfigPath) {
180
+ console.error(formatError(ERROR_CODES.E1001, '--runtime-config requires a path argument'));
181
+ process.exit(1);
182
+ }
183
+ runtimeConfigPath = resolve(runtimeConfigPath);
184
+ i++;
185
+ break;
186
+ }
187
+ case '--model': {
188
+ model = args[i + 1];
189
+ if (!model) {
190
+ console.error(formatError(ERROR_CODES.E1001, '--model requires a model name argument'));
191
+ process.exit(1);
192
+ }
193
+ i++;
194
+ break;
195
+ }
196
+ case '--agent-provider': {
197
+ agentProvider = args[i + 1];
198
+ if (!agentProvider) {
199
+ console.error(formatError(ERROR_CODES.E1001, '--agent-provider requires a provider name argument'));
200
+ process.exit(1);
201
+ }
202
+ i++;
203
+ break;
204
+ }
205
+ case '--generic-prompt': {
206
+ genericPrompt = args[i + 1];
207
+ if (!genericPrompt) {
208
+ console.error(formatError(ERROR_CODES.E1001, '--generic-prompt requires a filename argument'));
209
+ process.exit(1);
210
+ }
211
+ i++;
212
+ break;
213
+ }
214
+ case '--log-level': {
215
+ logLevel = args[i + 1];
216
+ if (!logLevel) {
217
+ console.error(formatError(ERROR_CODES.E1001, '--log-level requires a level argument (error, warn, info, debug, trace)'));
218
+ process.exit(1);
219
+ }
220
+ i++;
221
+ break;
222
+ }
223
+ case '--log-file': {
224
+ logFile = args[i + 1];
225
+ if (!logFile) {
226
+ console.error(formatError(ERROR_CODES.E1001, '--log-file requires a path argument'));
227
+ process.exit(1);
228
+ }
229
+ logFile = resolve(logFile);
230
+ i++;
231
+ break;
232
+ }
233
+ case '--log-type': {
234
+ logType = args[i + 1];
235
+ if (!logType) {
236
+ console.error(formatError(ERROR_CODES.E1001, '--log-type requires a type argument'));
237
+ process.exit(1);
238
+ }
239
+ i++;
240
+ break;
241
+ }
242
+ case '--diff-ref': {
243
+ diffRef = args[i + 1];
244
+ if (!diffRef) {
245
+ console.error(formatError(ERROR_CODES.E1001, '--diff-ref requires a git ref argument'));
246
+ process.exit(1);
247
+ }
248
+ i++;
249
+ break;
250
+ }
251
+ case '--diff-file': {
252
+ diffFile = args[i + 1];
253
+ if (!diffFile) {
254
+ console.error(formatError(ERROR_CODES.E1001, '--diff-file requires a path argument'));
255
+ process.exit(1);
256
+ }
257
+ diffFile = resolve(diffFile);
258
+ i++;
259
+ break;
260
+ }
261
+ case '--budget-limit-cost': {
262
+ const raw = args[i + 1];
263
+ if (!raw) {
264
+ console.error(formatError(ERROR_CODES.E1001, '--budget-limit-cost requires a number argument (USD)'));
265
+ process.exit(1);
266
+ }
267
+ const n = Number(raw);
268
+ if (!Number.isFinite(n) || n <= 0) {
269
+ console.error(formatError(ERROR_CODES.E1001, `--budget-limit-cost must be a positive number (got "${raw}")`));
270
+ process.exit(1);
271
+ }
272
+ budgetLimitCost = n;
273
+ i++;
274
+ break;
275
+ }
276
+ case '--budget-limit-tokens': {
277
+ const raw = args[i + 1];
278
+ if (!raw) {
279
+ console.error(formatError(ERROR_CODES.E1001, '--budget-limit-tokens requires a number argument'));
280
+ process.exit(1);
281
+ }
282
+ const n = Number(raw);
283
+ if (!Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) {
284
+ console.error(formatError(ERROR_CODES.E1001, `--budget-limit-tokens must be a positive integer (got "${raw}")`));
285
+ process.exit(1);
286
+ }
287
+ budgetLimitTokens = n;
288
+ i++;
289
+ break;
290
+ }
291
+ // --fail-on-check-failure and --debug are handled above via includes()
292
+ }
293
+ }
294
+ return {
295
+ repositoryPath, configDir, outputPath, outputFormat,
296
+ failOnCheckFailure, debug, logLevel, logFile, logType,
297
+ runtimeConfigPath, model, agentProvider, genericPrompt,
298
+ diffRef, diffFile,
299
+ budgetLimitCost, budgetLimitTokens,
300
+ };
301
+ }
302
+ async function createProvider(useMock, agentProviderName, modelOverride) {
303
+ if (useMock) {
304
+ logProgress(TAG, `Mock provider enabled via AGHAST_MOCK_AI=${process.env.AGHAST_MOCK_AI}`);
305
+ const provider = await createMockProvider();
306
+ // Honour --model in mock mode so cost-calculation tests can target a known
307
+ // pricing entry. Defaults to MOCK_MODEL_NAME.
308
+ const effectiveModel = modelOverride ?? MOCK_MODEL_NAME;
309
+ provider.setModel?.(effectiveModel);
310
+ return { provider, modelName: effectiveModel };
311
+ }
312
+ const provider = createProviderByName(agentProviderName);
313
+ await provider.initialize({
314
+ apiKey: process.env.ANTHROPIC_API_KEY,
315
+ model: modelOverride,
316
+ });
317
+ const modelName = provider.getModelName?.() ?? DEFAULT_MODEL;
318
+ return { provider, modelName };
319
+ }
320
+ /**
321
+ * Validate that the config directory has the required structure.
322
+ * Returns early with a clear error message if anything is missing.
323
+ */
324
+ async function validateConfigDir(configDir) {
325
+ // Check checks-config.json exists
326
+ const registryPath = resolve(configDir, 'checks-config.json');
327
+ try {
328
+ await stat(registryPath);
329
+ }
330
+ catch {
331
+ console.error(formatError(ERROR_CODES.E2002, `Config directory is missing checks-config.json: ${registryPath}`));
332
+ console.error(` Use 'aghast new-check --config-dir ${configDir}' to bootstrap a config directory.`);
333
+ process.exit(1);
334
+ }
335
+ // Check checks/ directory exists
336
+ const checksPath = resolve(configDir, 'checks');
337
+ try {
338
+ const checksStat = await stat(checksPath);
339
+ if (!checksStat.isDirectory()) {
340
+ console.error(formatError(ERROR_CODES.E2002, `${checksPath} exists but is not a directory`));
341
+ process.exit(1);
342
+ }
343
+ }
344
+ catch {
345
+ console.error(formatError(ERROR_CODES.E2002, `Config directory is missing checks/ folder: ${checksPath}`));
346
+ console.error(` Use 'aghast new-check --config-dir ${configDir}' to add checks.`);
347
+ process.exit(1);
348
+ }
349
+ // Check that checks/ has at least one subfolder
350
+ const entries = await readdir(checksPath);
351
+ if (entries.length === 0) {
352
+ console.error(formatError(ERROR_CODES.E2003, `No checks found in ${checksPath}`));
353
+ console.error(` Use 'aghast new-check --config-dir ${configDir}' to add checks.`);
354
+ process.exit(1);
355
+ }
356
+ }
357
+ export async function runScan(args) {
358
+ const globalTimer = createTimer();
359
+ const parsed = parseArgs(args);
360
+ // --config-dir is required (CLI flag > AGHAST_CONFIG_DIR env var)
361
+ const rawConfigDir = parsed.configDir || process.env.AGHAST_CONFIG_DIR;
362
+ if (!rawConfigDir) {
363
+ console.error(formatError(ERROR_CODES.E2001, "--config-dir is required (or set AGHAST_CONFIG_DIR). Use 'aghast new-check --config-dir <path>' to create a config directory."));
364
+ process.exit(1);
365
+ }
366
+ const configDir = resolve(rawConfigDir);
367
+ // Validate config directory structure
368
+ await validateConfigDir(configDir);
369
+ // Load runtime configuration
370
+ let runtimeConfig;
371
+ try {
372
+ runtimeConfig = await loadRuntimeConfig(configDir, parsed.runtimeConfigPath);
373
+ }
374
+ catch (err) {
375
+ console.error(formatError(ERROR_CODES.E2005, err instanceof Error ? err.message : String(err)));
376
+ process.exit(1);
377
+ }
378
+ // Resolve log level: --log-level > AGHAST_LOG_LEVEL > runtime config > --debug/AGHAST_DEBUG > default
379
+ const debug = parsed.debug || process.env.AGHAST_DEBUG === 'true';
380
+ const resolvedLogLevel = parsed.logLevel ?? (process.env.AGHAST_LOG_LEVEL || undefined) ?? runtimeConfig.logging?.level ?? (debug ? 'debug' : DEFAULT_LOG_LEVEL);
381
+ if (resolvedLogLevel !== 'silent' && !isValidLogLevel(resolvedLogLevel)) {
382
+ console.error(formatError(ERROR_CODES.E1001, `Invalid log level "${resolvedLogLevel}". Valid levels: error, warn, info, debug, trace`));
383
+ process.exit(1);
384
+ }
385
+ setLogLevel(resolvedLogLevel);
386
+ // Resolve log file: --log-file > AGHAST_LOG_FILE > runtime config
387
+ const resolvedLogFile = parsed.logFile ?? (process.env.AGHAST_LOG_FILE || undefined) ?? (runtimeConfig.logging?.logFile ? resolve(runtimeConfig.logging.logFile) : undefined);
388
+ if (resolvedLogFile) {
389
+ const resolvedLogType = parsed.logType ?? (process.env.AGHAST_LOG_TYPE || undefined) ?? runtimeConfig.logging?.logType ?? DEFAULT_LOG_TYPE;
390
+ const availableTypes = getAvailableLogTypes();
391
+ if (!availableTypes.includes(resolvedLogType)) {
392
+ console.error(formatError(ERROR_CODES.E1001, `Unknown log type "${resolvedLogType}". Available types: ${availableTypes.join(', ')}`));
393
+ process.exit(1);
394
+ }
395
+ initFileHandler(resolve(resolvedLogFile), resolvedLogType);
396
+ }
397
+ // Resolve repository path — required
398
+ if (!parsed.repositoryPath) {
399
+ console.error(formatError(ERROR_CODES.E1003, '<repo-path> is required'));
400
+ process.exit(1);
401
+ }
402
+ // Validate repository path exists and is a directory
403
+ try {
404
+ const repoStat = await stat(parsed.repositoryPath);
405
+ if (!repoStat.isDirectory()) {
406
+ console.error(formatError(ERROR_CODES.E4001, `Repository path is not a directory: ${parsed.repositoryPath}`));
407
+ process.exit(1);
408
+ }
409
+ }
410
+ catch (err) {
411
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
412
+ console.error(formatError(ERROR_CODES.E4001, `Repository path does not exist: ${parsed.repositoryPath}`));
413
+ process.exit(1);
414
+ }
415
+ throw err;
416
+ }
417
+ // Resolve output format: CLI > runtime config > default
418
+ const resolvedOutputFormat = parsed.outputFormat ?? runtimeConfig.reporting?.outputFormat ?? DEFAULT_OUTPUT_FORMAT;
419
+ // Resolve formatter early — fail fast on unknown format
420
+ const formatter = getFormatter(resolvedOutputFormat);
421
+ // Treat AGHAST_MOCK_AI=false (or empty) as disabled; any other truthy value enables mock mode
422
+ const mockAiEnv = process.env.AGHAST_MOCK_AI;
423
+ const useMock = !!(mockAiEnv && mockAiEnv !== 'false');
424
+ // ─── Load and filter checks BEFORE AI validation ───
425
+ // Checks always live in <config-dir>/checks/
426
+ const checksDirs = [resolve(configDir, 'checks')];
427
+ // Resolve generic prompt: CLI > env > runtime config > default (handled in buildPrompt)
428
+ const genericPrompt = parsed.genericPrompt ?? process.env.AGHAST_GENERIC_PROMPT ?? runtimeConfig.genericPrompt;
429
+ logDebug(TAG, `Config dir: ${configDir}, checks dir: ${checksDirs[0]}`);
430
+ // Two-layer config loading
431
+ const registry = await loadCheckRegistry(configDir);
432
+ const checkFolders = await discoverCheckFolders(checksDirs);
433
+ if (checkFolders.size === 0) {
434
+ console.error(formatError(ERROR_CODES.E2003, `No valid checks found in ${checksDirs[0]}. Each check needs a <id>/<id>.json file.`));
435
+ process.exit(1);
436
+ }
437
+ const allChecks = await resolveChecks(registry, checkFolders);
438
+ // Analyze repository to get remote URL for check filtering
439
+ const effectiveRepoPath = parsed.repositoryPath;
440
+ const repoAnalysis = await analyzeRepository(effectiveRepoPath);
441
+ const repoUrl = repoAnalysis?.repository.remoteUrl ?? effectiveRepoPath;
442
+ const matchingChecks = filterChecksForRepository(allChecks, repoUrl);
443
+ logProgress(TAG, `Found ${matchingChecks.length} matching checks (of ${allChecks.length} total)`);
444
+ if (matchingChecks.length === 0) {
445
+ logProgress(TAG, 'No matching checks found for this repository');
446
+ }
447
+ // Validate and load check details
448
+ const checksWithDetails = [];
449
+ for (const check of matchingChecks) {
450
+ // instructionsFile is already absolute from resolveChecks — validate against ''
451
+ const validation = await validateCheck(check, '');
452
+ if (!validation.valid) {
453
+ logProgress(TAG, `Skipping invalid check "${check.id}": ${validation.errors.join(', ')}`);
454
+ continue;
455
+ }
456
+ // checkTarget rules already resolved by resolveChecks — no additional path resolution needed
457
+ // Checks that don't require instructions use synthetic details (unless they provided one optionally).
458
+ // Built-in analysis modes (false-positive-validation, general-vuln-discovery) also don't need instructions.
459
+ const builtInMode = check.checkTarget?.analysisMode === 'false-positive-validation'
460
+ || check.checkTarget?.analysisMode === 'general-vuln-discovery';
461
+ if ((!getCheckType(check.checkTarget?.type).needsInstructions || builtInMode) && !check.instructionsFile) {
462
+ checksWithDetails.push({
463
+ check,
464
+ details: { id: check.id, name: check.name, overview: '', content: '' },
465
+ });
466
+ continue;
467
+ }
468
+ const details = await loadCheckDetails(check, '');
469
+ // Fall back to JSON definition name if markdown has no ### heading
470
+ if (details.name === 'Unknown Check') {
471
+ details.name = check.name;
472
+ }
473
+ checksWithDetails.push({ check, details });
474
+ }
475
+ // ─── Validate --generic-prompt with mixed discovery types ───
476
+ if (genericPrompt) {
477
+ const discoveryTypes = new Set();
478
+ for (const c of checksWithDetails) {
479
+ const d = c.check.checkTarget?.discovery;
480
+ if (d)
481
+ discoveryTypes.add(d);
482
+ }
483
+ if (discoveryTypes.size > 1) {
484
+ console.error(formatError(ERROR_CODES.E2004, `--generic-prompt cannot be used when checks have different discovery types (found: ${[...discoveryTypes].join(', ')}). Each discovery type uses its own default generic prompt.`));
485
+ process.exit(1);
486
+ }
487
+ }
488
+ // ─── Determine which prerequisites are needed ───
489
+ const needsAI = checksWithDetails.some(c => getCheckType(c.check.checkTarget?.type).needsAI);
490
+ const needsSemgrep = checksWithDetails.some(c => c.check.checkTarget?.discovery === 'semgrep');
491
+ const needsOpenant = checksWithDetails.some(c => c.check.checkTarget?.discovery === 'openant');
492
+ // ─── Resolve diff source ───
493
+ // Precedence: CLI --diff-ref/--diff-file > AGHAST_DIFF_REF > runtime config diffRef.
494
+ // (Check-level diffRef is applied per-check inside the scan runner.)
495
+ const resolvedDiffRef = parsed.diffRef ?? (process.env.AGHAST_DIFF_REF || undefined) ?? runtimeConfig.diffRef;
496
+ const resolvedDiffFile = parsed.diffFile;
497
+ // OpenAnt is needed for diff filtering. Required whenever a diff source is
498
+ // available and at least one check has a discovery that supports the filter
499
+ // and hasn't opted out.
500
+ const hasRuntimeDiffSource = resolvedDiffRef !== undefined || resolvedDiffFile !== undefined;
501
+ const hasAnyCheckLevelDiffRef = checksWithDetails.some(c => c.check.checkTarget?.diffRef !== undefined);
502
+ const diffSourceAvailable = hasRuntimeDiffSource || hasAnyCheckLevelDiffRef;
503
+ // Drive supportedDiscovery off the registry rather than a hardcoded list —
504
+ // new discoveries pick up their own behaviour via supportsDiffFilter without
505
+ // this gate having to be kept in sync.
506
+ const registeredDiscoveries = new Set(getRegisteredDiscoveries());
507
+ const needsDiffFilter = diffSourceAvailable && checksWithDetails.some(c => {
508
+ const ct = c.check.checkTarget;
509
+ if (!ct?.discovery || !registeredDiscoveries.has(ct.discovery))
510
+ return false;
511
+ const supportedDiscovery = getDiscovery(ct.discovery).supportsDiffFilter;
512
+ const optedIn = ct.diffFilter !== false;
513
+ const hasSource = hasRuntimeDiffSource || ct.diffRef !== undefined;
514
+ return supportedDiscovery && optedIn && hasSource;
515
+ });
516
+ // ─── Conditional agent provider setup ───
517
+ const agentProviderName = parsed.agentProvider ?? runtimeConfig.agentProvider?.name ?? DEFAULT_PROVIDER_NAME;
518
+ if (needsAI && !useMock) {
519
+ // Validate agent provider name before checking credentials (config errors before auth errors)
520
+ if (!getProviderNames().includes(agentProviderName)) {
521
+ console.error(formatError(ERROR_CODES.E3002, `Unknown agent provider "${agentProviderName}". Supported providers: ${getProviderNames().join(', ')}`));
522
+ process.exit(1);
523
+ }
524
+ // Validate provider-specific prerequisites (API keys, binaries, etc.)
525
+ try {
526
+ const tempProvider = createProviderByName(agentProviderName);
527
+ tempProvider.checkPrerequisites?.();
528
+ }
529
+ catch (err) {
530
+ console.error(formatError(ERROR_CODES.E3001, err instanceof Error ? err.message : String(err)));
531
+ process.exit(1);
532
+ }
533
+ }
534
+ // Resolve model precedence: CLI --model > env AGHAST_AI_MODEL > runtime config > default
535
+ const modelOverride = parsed.model ?? process.env.AGHAST_AI_MODEL ?? runtimeConfig.agentProvider?.model;
536
+ let provider;
537
+ let modelName;
538
+ if (needsAI) {
539
+ ({ provider, modelName } = await createProvider(useMock, agentProviderName, modelOverride));
540
+ logProgress(TAG, `Using model: ${modelName}`);
541
+ }
542
+ // ─── Conditional Semgrep verification (needed for semgrep discovery) ───
543
+ if (needsSemgrep && !process.env.AGHAST_MOCK_SEMGREP) {
544
+ try {
545
+ await verifySemgrepInstalled();
546
+ }
547
+ catch (err) {
548
+ console.error(formatError(ERROR_CODES.E5001, err instanceof Error ? err.message : String(err)));
549
+ process.exit(1);
550
+ }
551
+ }
552
+ // ─── Conditional OpenAnt verification ───
553
+ // `openant` discovery is a hard requirement: without OpenAnt there are no
554
+ // targets. Diff filtering on other discoveries is a soft requirement: if
555
+ // OpenAnt is missing we fall back to a depth-0 filter (file+line overlap
556
+ // only, no call-graph flow) and log a clear warning so the mode is visible.
557
+ let openantAvailable = true;
558
+ if (needsOpenant && !process.env.AGHAST_OPENANT_DATASET) {
559
+ try {
560
+ await verifyOpenAntInstalled();
561
+ }
562
+ catch (err) {
563
+ console.error(formatError(ERROR_CODES.E6001, err instanceof Error ? err.message : String(err)));
564
+ process.exit(1);
565
+ }
566
+ }
567
+ else if (needsDiffFilter && !process.env.AGHAST_OPENANT_DATASET) {
568
+ try {
569
+ await verifyOpenAntInstalled();
570
+ }
571
+ catch {
572
+ openantAvailable = false;
573
+ logProgress(TAG, 'OpenAnt is not installed — diff filter will run in depth-0 mode ' +
574
+ '(findings kept only if their file and line range appear in the diff; ' +
575
+ 'direct callers/callees of changed code are NOT included). ' +
576
+ 'Install OpenAnt (https://github.com/knostic/OpenAnt/) or set ' +
577
+ 'AGHAST_OPENANT_DATASET to a prebuilt dataset for depth-1 filtering with call-graph flow.');
578
+ }
579
+ }
580
+ // ─── Pricing + budget setup ───
581
+ const defaultPricing = await loadDefaultPricing();
582
+ const pricing = mergePricing(defaultPricing, runtimeConfig.pricing);
583
+ const budgetLimits = (() => {
584
+ const cliCost = parsed.budgetLimitCost;
585
+ const cliTokens = parsed.budgetLimitTokens;
586
+ const cfg = runtimeConfig.budget;
587
+ if (cliCost === undefined && cliTokens === undefined && !cfg)
588
+ return undefined;
589
+ const out = {};
590
+ const perScan = { ...(cfg?.perScan ?? {}) };
591
+ if (cliCost !== undefined)
592
+ perScan.maxCostUsd = cliCost;
593
+ if (cliTokens !== undefined)
594
+ perScan.maxTokens = cliTokens;
595
+ if (perScan.maxCostUsd !== undefined || perScan.maxTokens !== undefined) {
596
+ out.perScan = perScan;
597
+ }
598
+ if (cfg?.perPeriod && cfg.perPeriod.window && cfg.perPeriod.maxCostUsd !== undefined) {
599
+ out.perPeriod = { window: cfg.perPeriod.window, maxCostUsd: cfg.perPeriod.maxCostUsd };
600
+ }
601
+ if (cfg?.thresholds)
602
+ out.thresholds = cfg.thresholds;
603
+ return Object.keys(out).length > 0 ? out : undefined;
604
+ })();
605
+ // Pre-load history for period budget checks (skip when no period limit set)
606
+ let scanHistoryForBudget;
607
+ if (budgetLimits?.perPeriod) {
608
+ try {
609
+ scanHistoryForBudget = await queryScanHistory();
610
+ }
611
+ catch (err) {
612
+ logDebug(TAG, `Could not load scan history for budget check: ${err instanceof Error ? err.message : String(err)}`);
613
+ }
614
+ }
615
+ try {
616
+ const outcome = await runMultiScanWithCost({
617
+ repositoryPath: effectiveRepoPath,
618
+ checks: checksWithDetails,
619
+ agentProvider: provider,
620
+ modelName: needsAI ? modelName : undefined,
621
+ repositoryInfo: repoAnalysis?.repository,
622
+ agentProviderName: needsAI ? (useMock ? 'mock' : agentProviderName) : undefined,
623
+ configDir,
624
+ genericPrompt,
625
+ diffRef: resolvedDiffRef,
626
+ diffFile: resolvedDiffFile,
627
+ openantAvailable,
628
+ pricing,
629
+ budgetLimits,
630
+ scanHistory: scanHistoryForBudget,
631
+ isLocalClaude: process.env.AGHAST_LOCAL_CLAUDE === 'true',
632
+ });
633
+ const results = outcome.results;
634
+ // Resolve output path: --output flag > runtime config dir > default
635
+ let resolvedOutputPath;
636
+ if (parsed.outputPath) {
637
+ resolvedOutputPath = parsed.outputPath;
638
+ }
639
+ else if (runtimeConfig.reporting?.outputDirectory) {
640
+ const dir = resolve(runtimeConfig.reporting.outputDirectory);
641
+ resolvedOutputPath = resolve(dir, 'security_checks_results' + formatter.fileExtension);
642
+ }
643
+ else {
644
+ resolvedOutputPath = resolve(effectiveRepoPath, 'security_checks_results' + formatter.fileExtension);
645
+ }
646
+ await mkdir(dirname(resolvedOutputPath), { recursive: true });
647
+ await writeFile(resolvedOutputPath, formatter.format(results), 'utf-8');
648
+ // Summary output
649
+ const statusIcon = results.summary.failedChecks > 0
650
+ ? 'ISSUES DETECTED'
651
+ : results.summary.flaggedChecks > 0
652
+ ? 'REVIEW REQUIRED'
653
+ : results.summary.errorChecks > 0
654
+ ? 'SCAN ERROR'
655
+ : 'NO ISSUES DETECTED';
656
+ console.log('');
657
+ console.log('='.repeat(60));
658
+ console.log(`AGHAST Scan Complete: ${colorStatus(statusIcon)}`);
659
+ console.log('='.repeat(60));
660
+ console.log(` Total checks: ${results.summary.totalChecks}`);
661
+ console.log(` Passed: ${results.summary.passedChecks}`);
662
+ console.log(` Failed: ${results.summary.failedChecks}`);
663
+ console.log(` Flagged: ${results.summary.flaggedChecks}`);
664
+ console.log(` Errors: ${results.summary.errorChecks}`);
665
+ console.log(` Total issues: ${results.summary.totalIssues}`);
666
+ if (results.tokenUsage) {
667
+ const tu = results.tokenUsage;
668
+ const cacheSegments = [
669
+ tu.cacheReadInputTokens !== undefined ? ` cache-read: ${tu.cacheReadInputTokens.toLocaleString()}` : '',
670
+ tu.cacheCreationInputTokens !== undefined ? ` cache-write: ${tu.cacheCreationInputTokens.toLocaleString()}` : '',
671
+ tu.reasoningTokens !== undefined ? ` reasoning: ${tu.reasoningTokens.toLocaleString()}` : '',
672
+ ].filter(Boolean).join(',');
673
+ const tokenDetail = `(in: ${tu.inputTokens.toLocaleString()}, out: ${tu.outputTokens.toLocaleString()}${cacheSegments ? ',' + cacheSegments : ''})`;
674
+ console.log(` Tokens: ${tu.totalTokens.toLocaleString()} ${tokenDetail}`);
675
+ }
676
+ if (outcome.totalCostUsd > 0 || outcome.costSource === 'estimated-unpriced') {
677
+ const label = formatCostSourceLabel(outcome.costSource, outcome.costReportedBy, outcome.costCoveredBySubscription);
678
+ const equiv = outcome.costCoveredBySubscription ? ' equivalent' : '';
679
+ console.log(` Cost: $${outcome.totalCostUsd.toFixed(4)}${equiv} ${label}`);
680
+ }
681
+ console.log(` Duration: ${globalTimer.elapsedStr()}`);
682
+ console.log(` Results: ${resolvedOutputPath}`);
683
+ console.log('='.repeat(60));
684
+ // Persist the scan record to history (best-effort — never blocks exit).
685
+ // We DO save the record even when the scan was aborted by budget: per-period
686
+ // budgets aggregate over historical scans, so the partial cost incurred
687
+ // before the abort must be recorded — otherwise a user could repeatedly
688
+ // hit the budget abort and still consume more total spend than the period
689
+ // limit allows.
690
+ try {
691
+ const record = {
692
+ scanId: results.scanId,
693
+ startedAt: results.startTime,
694
+ endedAt: results.endTime,
695
+ durationMs: results.executionTime,
696
+ repository: effectiveRepoPath,
697
+ repositoryUrl: repoAnalysis?.repository.remoteUrl,
698
+ models: outcome.models.length > 0 ? outcome.models : (modelName ? [modelName] : []),
699
+ tokenUsage: results.tokenUsage,
700
+ totalCost: outcome.totalCostUsd,
701
+ currency: outcome.currency,
702
+ costSource: outcome.costSource,
703
+ costReportedBy: outcome.costReportedBy,
704
+ costCoveredBySubscription: outcome.costCoveredBySubscription,
705
+ checks: results.summary.totalChecks,
706
+ issues: results.summary.totalIssues,
707
+ };
708
+ await saveScanRecord(record);
709
+ }
710
+ catch (err) {
711
+ logDebug(TAG, `Failed to save scan history: ${err instanceof Error ? err.message : String(err)}`);
712
+ }
713
+ // A budget abort is a deliberate failure mode the user opted into via
714
+ // --budget-limit-* / runtime config — it must always exit non-zero (and
715
+ // surface E7001 to stderr) regardless of --fail-on-check-failure. Doing
716
+ // otherwise would silently drop the abort signal in CI pipelines that
717
+ // rely on the exit code as a guardrail.
718
+ //
719
+ // Emit through both stderr (for terminal users / CI logs) AND the logging
720
+ // system (so --log-file captures the abort reason — console.error bypasses
721
+ // registered log handlers).
722
+ if (outcome.budgetAborted) {
723
+ const reason = outcome.budgetAbortReason ?? 'Budget limit exceeded';
724
+ console.error(formatError(ERROR_CODES.E7001, reason));
725
+ logProgress(TAG, `Scan aborted by budget: ${reason}`);
726
+ }
727
+ // Exit code based on --fail-on-check-failure flag or runtime config (spec Section 9.3),
728
+ // OR a budget abort.
729
+ const failOnCheckFailure = parsed.failOnCheckFailure || runtimeConfig.failOnCheckFailure === true;
730
+ const shouldFail = outcome.budgetAborted ||
731
+ (failOnCheckFailure && (results.summary.failedChecks > 0 || results.summary.errorChecks > 0));
732
+ await closeAllHandlers();
733
+ process.exit(shouldFail ? 1 : 0);
734
+ }
735
+ finally {
736
+ // Clean up provider resources (e.g. OpenCode server process)
737
+ if (provider && 'cleanup' in provider && typeof provider.cleanup === 'function') {
738
+ await provider.cleanup();
739
+ }
740
+ }
741
+ }
742
+ // Auto-run when executed directly (npm run scan / tsx src/index.ts), but not when imported by cli.ts.
743
+ if (!process.env._AGHAST_CLI) {
744
+ runScan(process.argv.slice(2)).catch(async (err) => {
745
+ const require = createRequire(import.meta.url);
746
+ const pkg = require('../package.json');
747
+ console.error('');
748
+ console.error(formatFatalError(err instanceof Error ? err.message : String(err), pkg.version));
749
+ logDebug(TAG, 'Error details', err);
750
+ await closeAllHandlers();
751
+ process.exit(1);
752
+ });
753
+ }
754
+ //# sourceMappingURL=index.js.map