jaku.sh 1.0.2 → 1.2.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.
@@ -1,4 +1,5 @@
1
1
  import { BaseAgent } from './base-agent.js';
2
+ import { allows, getSafetyMode } from '../utils/safety.js';
2
3
  import { BusinessRuleInferrer } from '../core/logic/business-rule-inferrer.js';
3
4
  import { PricingExploiter } from '../core/logic/pricing-exploiter.js';
4
5
  import { AccessBoundaryTester } from '../core/logic/access-boundary-tester.js';
@@ -40,12 +41,39 @@ export class LogicAgent extends BaseAgent {
40
41
  throw new Error('No surface inventory available — JAKU-CRAWL must run first');
41
42
  }
42
43
 
44
+ const safetyMode = getSafetyMode(config);
45
+ this._log(`Safety mode: ${safetyMode}`);
46
+
47
+ // Tests that issue real state-changing/mutating requests (or that the
48
+ // user has classified as destructive) require --aggressive. Under
49
+ // passive/safe-active they are skipped with a clear log line.
50
+ const allowDestructive = allows(config, 'aggressive');
51
+ // Active but non-destructive probing requires at least safe-active.
52
+ const allowActive = allows(config, 'safe-active');
53
+
54
+ const skipDestructive = (label, phase, pct) => {
55
+ this._log(`${label} skipped — requires --aggressive safety mode (current: ${safetyMode})`);
56
+ this.progress(phase, `${label} skipped (safe mode)`, pct);
57
+ };
58
+ const skipActive = (label, phase, pct) => {
59
+ this._log(`${label} skipped — requires active probing (current: ${safetyMode} mode)`);
60
+ this.progress(phase, `${label} skipped (passive mode)`, pct);
61
+ };
62
+
43
63
  // Phase 1: Infer business rules
44
64
  this.progress('infer', 'Inferring business rules from surface inventory...', 0);
45
65
 
46
66
  const inferrer = new BusinessRuleInferrer(logger);
47
67
  const businessContext = inferrer.infer(surfaceInventory);
48
68
 
69
+ // Optional, additive LLM domain/invariant inference (no-op when disabled).
70
+ if (context.llmClient?.isEnabled?.()) {
71
+ await inferrer.augmentWithLLM(businessContext, surfaceInventory, context.llmClient);
72
+ if (businessContext.llmInsights?.invariants?.length) {
73
+ this._log(`LLM inferred ${businessContext.llmInsights.invariants.length} candidate business invariants`);
74
+ }
75
+ }
76
+
49
77
  const activeDomains = Object.entries(businessContext.domains)
50
78
  .filter(([, items]) => items.length > 0)
51
79
  .map(([domain]) => domain);
@@ -62,123 +90,163 @@ export class LogicAgent extends BaseAgent {
62
90
  const totalPhases = 11;
63
91
  let completed = 1;
64
92
 
65
- // Phase 2: Pricing exploitation
66
- this.progress('pricing', 'Testing pricing & payment logic...', (completed / totalPhases) * 100);
67
- try {
68
- const exploiter = new PricingExploiter(logger);
69
- const pricingFindings = await exploiter.exploit(businessContext);
70
- this.addFindings(pricingFindings);
71
- this._log(`Pricing: ${pricingFindings.length} issues`);
72
- } catch (err) {
73
- this._log(`Pricing testing failed: ${err.message}`, 'error');
93
+ // Phase 2: Pricing exploitation (DESTRUCTIVE — sends mutating requests)
94
+ if (allowDestructive) {
95
+ this.progress('pricing', 'Testing pricing & payment logic...', (completed / totalPhases) * 100);
96
+ try {
97
+ const exploiter = new PricingExploiter(logger);
98
+ const pricingFindings = await exploiter.exploit(businessContext);
99
+ this.addFindings(pricingFindings);
100
+ this._log(`Pricing: ${pricingFindings.length} issues`);
101
+ } catch (err) {
102
+ this._log(`Pricing testing failed: ${err.message}`, 'error');
103
+ }
104
+ } else {
105
+ skipDestructive('Pricing exploitation', 'pricing', (completed / totalPhases) * 100);
74
106
  }
75
107
  completed++;
76
108
 
77
- // Phase 3: Access boundary testing
78
- this.progress('access', 'Testing access control boundaries...', (completed / totalPhases) * 100);
79
- try {
80
- const tester = new AccessBoundaryTester(logger);
81
- const accessFindings = await tester.test(businessContext, surfaceInventory);
82
- this.addFindings(accessFindings);
83
- this._log(`Access: ${accessFindings.length} issues`);
84
- } catch (err) {
85
- this._log(`Access boundary testing failed: ${err.message}`, 'error');
109
+ // Phase 3: Access boundary testing (active, non-destructive)
110
+ if (allowActive) {
111
+ this.progress('access', 'Testing access control boundaries...', (completed / totalPhases) * 100);
112
+ try {
113
+ const tester = new AccessBoundaryTester(logger);
114
+ const accessFindings = await tester.test(businessContext, surfaceInventory);
115
+ this.addFindings(accessFindings);
116
+ this._log(`Access: ${accessFindings.length} issues`);
117
+ } catch (err) {
118
+ this._log(`Access boundary testing failed: ${err.message}`, 'error');
119
+ }
120
+ } else {
121
+ skipActive('Access boundary testing', 'access', (completed / totalPhases) * 100);
86
122
  }
87
123
  completed++;
88
124
 
89
- // Phase 4: Workflow enforcement
90
- this.progress('workflow', 'Testing workflow enforcement...', (completed / totalPhases) * 100);
91
- try {
92
- const enforcer = new WorkflowEnforcer(logger);
93
- const workflowFindings = await enforcer.enforce(businessContext, surfaceInventory);
94
- this.addFindings(workflowFindings);
95
- this._log(`Workflow: ${workflowFindings.length} issues`);
96
- } catch (err) {
97
- this._log(`Workflow testing failed: ${err.message}`, 'error');
125
+ // Phase 4: Workflow enforcement (active, non-destructive)
126
+ if (allowActive) {
127
+ this.progress('workflow', 'Testing workflow enforcement...', (completed / totalPhases) * 100);
128
+ try {
129
+ const enforcer = new WorkflowEnforcer(logger);
130
+ const workflowFindings = await enforcer.enforce(businessContext, surfaceInventory);
131
+ this.addFindings(workflowFindings);
132
+ this._log(`Workflow: ${workflowFindings.length} issues`);
133
+ } catch (err) {
134
+ this._log(`Workflow testing failed: ${err.message}`, 'error');
135
+ }
136
+ } else {
137
+ skipActive('Workflow enforcement', 'workflow', (completed / totalPhases) * 100);
98
138
  }
99
139
  completed++;
100
140
 
101
- // Phase 5: Race condition detection
102
- this.progress('race', 'Testing for race conditions...', (completed / totalPhases) * 100);
103
- try {
104
- const detector = new RaceConditionDetector(logger);
105
- const raceFindings = await detector.detect(businessContext, surfaceInventory);
106
- this.addFindings(raceFindings);
107
- this._log(`Race conditions: ${raceFindings.length} issues`);
108
- } catch (err) {
109
- this._log(`Race condition testing failed: ${err.message}`, 'error');
141
+ // Phase 5: Race condition detection (DESTRUCTIVE — fires concurrent mutating requests)
142
+ if (allowDestructive) {
143
+ this.progress('race', 'Testing for race conditions...', (completed / totalPhases) * 100);
144
+ try {
145
+ const detector = new RaceConditionDetector(logger);
146
+ const raceFindings = await detector.detect(businessContext, surfaceInventory);
147
+ this.addFindings(raceFindings);
148
+ this._log(`Race conditions: ${raceFindings.length} issues`);
149
+ } catch (err) {
150
+ this._log(`Race condition testing failed: ${err.message}`, 'error');
151
+ }
152
+ } else {
153
+ skipDestructive('Race condition detection', 'race', (completed / totalPhases) * 100);
110
154
  }
111
155
  completed++;
112
156
 
113
- // Phase 6: Abuse pattern scanning
114
- this.progress('abuse', 'Scanning for abuse patterns...', (completed / totalPhases) * 100);
115
- try {
116
- const scanner = new AbusePatternScanner(logger);
117
- const abuseFindings = await scanner.scan(businessContext, surfaceInventory);
118
- this.addFindings(abuseFindings);
119
- this._log(`Abuse patterns: ${abuseFindings.length} issues`);
120
- } catch (err) {
121
- this._log(`Abuse pattern scanning failed: ${err.message}`, 'error');
157
+ // Phase 6: Abuse pattern scanning (active, non-destructive)
158
+ if (allowActive) {
159
+ this.progress('abuse', 'Scanning for abuse patterns...', (completed / totalPhases) * 100);
160
+ try {
161
+ const scanner = new AbusePatternScanner(logger);
162
+ const abuseFindings = await scanner.scan(businessContext, surfaceInventory);
163
+ this.addFindings(abuseFindings);
164
+ this._log(`Abuse patterns: ${abuseFindings.length} issues`);
165
+ } catch (err) {
166
+ this._log(`Abuse pattern scanning failed: ${err.message}`, 'error');
167
+ }
168
+ } else {
169
+ skipActive('Abuse pattern scanning', 'abuse', (completed / totalPhases) * 100);
122
170
  }
123
171
  completed++;
124
172
 
125
- // Phase 7: Coupon/promo abuse testing
126
- this.progress('coupon', 'Testing for coupon/promo abuse...', (completed / totalPhases) * 100);
127
- try {
128
- const tester = new CouponAbuseTester(logger);
129
- const couponFindings = await tester.test(businessContext, surfaceInventory);
130
- this.addFindings(couponFindings);
131
- this._log(`Coupon abuse: ${couponFindings.length} issues`);
132
- } catch (err) {
133
- this._log(`Coupon abuse testing failed: ${err.message}`, 'error');
173
+ // Phase 7: Coupon/promo abuse testing (gated to aggressive per safety policy)
174
+ if (allowDestructive) {
175
+ this.progress('coupon', 'Testing for coupon/promo abuse...', (completed / totalPhases) * 100);
176
+ try {
177
+ const tester = new CouponAbuseTester(logger);
178
+ const couponFindings = await tester.test(businessContext, surfaceInventory);
179
+ this.addFindings(couponFindings);
180
+ this._log(`Coupon abuse: ${couponFindings.length} issues`);
181
+ } catch (err) {
182
+ this._log(`Coupon abuse testing failed: ${err.message}`, 'error');
183
+ }
184
+ } else {
185
+ skipDestructive('Coupon/promo abuse testing', 'coupon', (completed / totalPhases) * 100);
134
186
  }
135
187
  completed++;
136
188
 
137
- // Phase 8: Cart manipulation testing
138
- this.progress('cart', 'Testing for cart manipulation...', (completed / totalPhases) * 100);
139
- try {
140
- const tester = new CartManipulationTester(logger);
141
- const cartFindings = await tester.test(businessContext, surfaceInventory);
142
- this.addFindings(cartFindings);
143
- this._log(`Cart manipulation: ${cartFindings.length} issues`);
144
- } catch (err) {
145
- this._log(`Cart manipulation testing failed: ${err.message}`, 'error');
189
+ // Phase 8: Cart manipulation testing (gated to aggressive per safety policy)
190
+ if (allowDestructive) {
191
+ this.progress('cart', 'Testing for cart manipulation...', (completed / totalPhases) * 100);
192
+ try {
193
+ const tester = new CartManipulationTester(logger);
194
+ const cartFindings = await tester.test(businessContext, surfaceInventory);
195
+ this.addFindings(cartFindings);
196
+ this._log(`Cart manipulation: ${cartFindings.length} issues`);
197
+ } catch (err) {
198
+ this._log(`Cart manipulation testing failed: ${err.message}`, 'error');
199
+ }
200
+ } else {
201
+ skipDestructive('Cart manipulation testing', 'cart', (completed / totalPhases) * 100);
146
202
  }
147
203
  completed++;
148
204
 
149
- // Phase 9: Email enumeration testing
150
- this.progress('email-enum', 'Testing for email enumeration...', (completed / totalPhases) * 100);
151
- try {
152
- const tester = new EmailEnumerationTester(logger);
153
- const emailFindings = await tester.test(businessContext, surfaceInventory);
154
- this.addFindings(emailFindings);
155
- this._log(`Email enumeration: ${emailFindings.length} issues`);
156
- } catch (err) {
157
- this._log(`Email enumeration testing failed: ${err.message}`, 'error');
205
+ // Phase 9: Email enumeration testing (active, non-destructive)
206
+ if (allowActive) {
207
+ this.progress('email-enum', 'Testing for email enumeration...', (completed / totalPhases) * 100);
208
+ try {
209
+ const tester = new EmailEnumerationTester(logger);
210
+ const emailFindings = await tester.test(businessContext, surfaceInventory);
211
+ this.addFindings(emailFindings);
212
+ this._log(`Email enumeration: ${emailFindings.length} issues`);
213
+ } catch (err) {
214
+ this._log(`Email enumeration testing failed: ${err.message}`, 'error');
215
+ }
216
+ } else {
217
+ skipActive('Email enumeration testing', 'email-enum', (completed / totalPhases) * 100);
158
218
  }
159
219
  completed++;
160
220
 
161
- // Phase 10: Account takeover testing
162
- this.progress('account-takeover', 'Testing account takeover flows...', (completed / totalPhases) * 100);
163
- try {
164
- const tester = new AccountTakeoverTester(logger);
165
- const atoFindings = await tester.test(businessContext, surfaceInventory);
166
- this.addFindings(atoFindings);
167
- this._log(`Account takeover: ${atoFindings.length} issues`);
168
- } catch (err) {
169
- this._log(`Account takeover testing failed: ${err.message}`, 'error');
221
+ // Phase 10: Account takeover testing (gated to aggressive per safety policy)
222
+ if (allowDestructive) {
223
+ this.progress('account-takeover', 'Testing account takeover flows...', (completed / totalPhases) * 100);
224
+ try {
225
+ const tester = new AccountTakeoverTester(logger);
226
+ const atoFindings = await tester.test(businessContext, surfaceInventory);
227
+ this.addFindings(atoFindings);
228
+ this._log(`Account takeover: ${atoFindings.length} issues`);
229
+ } catch (err) {
230
+ this._log(`Account takeover testing failed: ${err.message}`, 'error');
231
+ }
232
+ } else {
233
+ skipDestructive('Account takeover testing', 'account-takeover', (completed / totalPhases) * 100);
170
234
  }
171
235
  completed++;
172
236
 
173
- // Phase 11: Feature flag bypass testing
174
- this.progress('feature-flags', 'Testing for feature flag bypass...', (completed / totalPhases) * 100);
175
- try {
176
- const tester = new FeatureFlagBypassTester(logger);
177
- const flagFindings = await tester.test(businessContext, surfaceInventory);
178
- this.addFindings(flagFindings);
179
- this._log(`Feature flag bypass: ${flagFindings.length} issues`);
180
- } catch (err) {
181
- this._log(`Feature flag bypass testing failed: ${err.message}`, 'error');
237
+ // Phase 11: Feature flag bypass testing (active, non-destructive)
238
+ if (allowActive) {
239
+ this.progress('feature-flags', 'Testing for feature flag bypass...', (completed / totalPhases) * 100);
240
+ try {
241
+ const tester = new FeatureFlagBypassTester(logger);
242
+ const flagFindings = await tester.test(businessContext, surfaceInventory);
243
+ this.addFindings(flagFindings);
244
+ this._log(`Feature flag bypass: ${flagFindings.length} issues`);
245
+ } catch (err) {
246
+ this._log(`Feature flag bypass testing failed: ${err.message}`, 'error');
247
+ }
248
+ } else {
249
+ skipActive('Feature flag bypass testing', 'feature-flags', (completed / totalPhases) * 100);
182
250
  }
183
251
  completed++;
184
252
 
@@ -1,5 +1,8 @@
1
1
  import { EventBus } from './event-bus.js';
2
2
  import { FindingsLedger } from './findings-ledger.js';
3
+ import { getVersion } from '../utils/version.js';
4
+ import { LLMClient } from '../core/llm/llm-client.js';
5
+ import { enrichCorrelation, triageFinding } from '../core/llm/augmentations.js';
3
6
 
4
7
  /**
5
8
  * Orchestrator — Central coordinator for the JAKU multi-agent system.
@@ -18,6 +21,11 @@ export class Orchestrator {
18
21
  this.eventBus = new EventBus();
19
22
  this.ledger = new FindingsLedger(this.eventBus);
20
23
 
24
+ // Optional LLM augmentation client (default OFF). Constructed once and
25
+ // shared with all agents via context so any agent can opt in. Returns
26
+ // null on every call when disabled/unavailable → callers degrade.
27
+ this.llmClient = new LLMClient(config, logger);
28
+
21
29
  this._agents = new Map(); // name → agent instance
22
30
  this._sharedContext = { // passed to all agents
23
31
  config,
@@ -25,6 +33,7 @@ export class Orchestrator {
25
33
  eventBus: this.eventBus,
26
34
  ledger: this.ledger,
27
35
  surfaceInventory: null, // set by JAKU-CRAWL
36
+ llmClient: this.llmClient, // optional; null-equivalent when disabled
28
37
  };
29
38
 
30
39
  this._startTime = null;
@@ -123,6 +132,10 @@ export class Orchestrator {
123
132
  const duration = Date.now() - this._startTime;
124
133
  const results = this._synthesize(duration);
125
134
 
135
+ // Optional LLM augmentation of synthesized results (additive, best-effort).
136
+ // The deterministic correlate()/dedup output above is the source of truth.
137
+ await this._llmAugment(results);
138
+
126
139
  this.eventBus.emit('scan:completed', {
127
140
  timestamp: new Date().toISOString(),
128
141
  duration,
@@ -230,6 +243,48 @@ export class Orchestrator {
230
243
  };
231
244
  }
232
245
 
246
+ /**
247
+ * Phase 2 — LLM augmentation of synthesized results. STRICTLY ADDITIVE:
248
+ * - Enriches attack-chain correlation narratives (keeps original as fallback).
249
+ * - Triages borderline findings (advisory `llm_triage` note only; never
250
+ * mutates the deterministic severity or pass/fail).
251
+ * No-op when the LLM client is disabled/over-budget (returns immediately).
252
+ */
253
+ async _llmAugment(results) {
254
+ const client = this.llmClient;
255
+ if (!client?.isEnabled?.()) return;
256
+
257
+ try {
258
+ // Enrich correlation narratives.
259
+ for (const c of results.correlations || []) {
260
+ const enriched = await enrichCorrelation(client, c);
261
+ if (enriched) {
262
+ c.narrative_llm = enriched;
263
+ c.narrative_source = 'llm';
264
+ }
265
+ }
266
+
267
+ // Triage borderline findings only (limit egress + budget).
268
+ const borderline = (results.deduplicated || results.findings || [])
269
+ .filter(f => this._isBorderline(f))
270
+ .slice(0, 15);
271
+ for (const f of borderline) {
272
+ const triage = await triageFinding(client, f);
273
+ if (triage) f.llm_triage = triage;
274
+ }
275
+ } catch (err) {
276
+ this.logger?.debug?.(`[LLM] augmentation skipped: ${err.message}`);
277
+ }
278
+ }
279
+
280
+ /** Heuristic: which findings are "borderline" and worth LLM triage. */
281
+ _isBorderline(f) {
282
+ if (!f) return false;
283
+ if (f.severity === 'medium' || f.severity === 'low') return true;
284
+ const text = `${f.title || ''} ${f.description || ''}`.toLowerCase();
285
+ return /\b(potential|possible|may|might|suspected|likely|reflected)\b/.test(text);
286
+ }
287
+
233
288
  /**
234
289
  * Get the status of all agents.
235
290
  */
@@ -255,7 +310,7 @@ export class Orchestrator {
255
310
  try {
256
311
  const payload = {
257
312
  agent: 'JAKU',
258
- version: '1.0.2',
313
+ version: getVersion(),
259
314
  target: this.config.target_url,
260
315
  timestamp: new Date().toISOString(),
261
316
  duration: results.duration,
@@ -1,4 +1,5 @@
1
1
  import { BaseAgent } from './base-agent.js';
2
+ import { allows, getSafetyMode } from '../utils/safety.js';
2
3
  import { HeaderAnalyzer } from '../core/security/header-analyzer.js';
3
4
  import { SecretDetector } from '../core/security/secret-detector.js';
4
5
  import { XSSScanner } from '../core/security/xss-scanner.js';
@@ -37,6 +38,13 @@ export class SecurityAgent extends BaseAgent {
37
38
  throw new Error('No surface inventory available — JAKU-CRAWL must run first');
38
39
  }
39
40
 
41
+ // Active attack probing (injecting payloads) requires at least
42
+ // safe-active. In passive mode we only run read-only/analysis checks.
43
+ const allowActive = allows(config, 'safe-active');
44
+ const safetyMode = getSafetyMode(config);
45
+ const skipActive = (label) =>
46
+ this._log(`${label} skipped — requires active probing (current: ${safetyMode} mode)`);
47
+
40
48
  const phases = [
41
49
  { name: 'headers', label: 'Analyzing security headers' },
42
50
  { name: 'secrets', label: 'Scanning for exposed secrets' },
@@ -82,27 +90,35 @@ export class SecurityAgent extends BaseAgent {
82
90
  }
83
91
  completedPhases++;
84
92
 
85
- // Phase 3: XSS Scanning
86
- this.progress(phases[2].name, phases[2].label, (completedPhases / phases.length) * 100);
87
- try {
88
- const scanner = new XSSScanner(logger);
89
- const findings = await scanner.scan(surfaceInventory);
90
- this.addFindings(findings);
91
- this._log(`XSS: ${findings.length} vulnerabilities`);
92
- } catch (err) {
93
- this._log(`XSS scanning failed: ${err.message}`, 'error');
93
+ // Phase 3: XSS Scanning (active probing)
94
+ if (allowActive) {
95
+ this.progress(phases[2].name, phases[2].label, (completedPhases / phases.length) * 100);
96
+ try {
97
+ const scanner = new XSSScanner(logger);
98
+ const findings = await scanner.scan(surfaceInventory);
99
+ this.addFindings(findings);
100
+ this._log(`XSS: ${findings.length} vulnerabilities`);
101
+ } catch (err) {
102
+ this._log(`XSS scanning failed: ${err.message}`, 'error');
103
+ }
104
+ } else {
105
+ skipActive('XSS scanning');
94
106
  }
95
107
  completedPhases++;
96
108
 
97
- // Phase 4: SQL Injection Probing
98
- this.progress(phases[3].name, phases[3].label, (completedPhases / phases.length) * 100);
99
- try {
100
- const prober = new SQLiProber(logger);
101
- const findings = await prober.probe(surfaceInventory);
102
- this.addFindings(findings);
103
- this._log(`SQLi: ${findings.length} vulnerabilities`);
104
- } catch (err) {
105
- this._log(`SQLi probing failed: ${err.message}`, 'error');
109
+ // Phase 4: SQL Injection Probing (active probing)
110
+ if (allowActive) {
111
+ this.progress(phases[3].name, phases[3].label, (completedPhases / phases.length) * 100);
112
+ try {
113
+ const prober = new SQLiProber(logger);
114
+ const findings = await prober.probe(surfaceInventory);
115
+ this.addFindings(findings);
116
+ this._log(`SQLi: ${findings.length} vulnerabilities`);
117
+ } catch (err) {
118
+ this._log(`SQLi probing failed: ${err.message}`, 'error');
119
+ }
120
+ } else {
121
+ skipActive('SQLi probing');
106
122
  }
107
123
  completedPhases++;
108
124
 
@@ -130,27 +146,35 @@ export class SecurityAgent extends BaseAgent {
130
146
  }
131
147
  completedPhases++;
132
148
 
133
- // Phase 7: Infrastructure Scan
134
- this.progress(phases[6].name, phases[6].label, (completedPhases / phases.length) * 100);
135
- try {
136
- const scanner = new InfraScanner(logger);
137
- const findings = await scanner.scan(surfaceInventory);
138
- this.addFindings(findings);
139
- this._log(`Infrastructure: ${findings.length} issues`);
140
- } catch (err) {
141
- this._log(`Infrastructure scan failed: ${err.message}`, 'error');
149
+ // Phase 7: Infrastructure Scan (active probing of admin/debug endpoints)
150
+ if (allowActive) {
151
+ this.progress(phases[6].name, phases[6].label, (completedPhases / phases.length) * 100);
152
+ try {
153
+ const scanner = new InfraScanner(logger);
154
+ const findings = await scanner.scan(surfaceInventory);
155
+ this.addFindings(findings);
156
+ this._log(`Infrastructure: ${findings.length} issues`);
157
+ } catch (err) {
158
+ this._log(`Infrastructure scan failed: ${err.message}`, 'error');
159
+ }
160
+ } else {
161
+ skipActive('Infrastructure scan');
142
162
  }
143
163
  completedPhases++;
144
164
 
145
- // Phase 8: File Upload Testing
146
- this.progress(phases[7].name, phases[7].label, (completedPhases / phases.length) * 100);
147
- try {
148
- const uploader = new FileUploadTester(logger);
149
- const findings = await uploader.test(surfaceInventory);
150
- this.addFindings(findings);
151
- this._log(`File uploads: ${findings.length} issues`);
152
- } catch (err) {
153
- this._log(`File upload testing failed: ${err.message}`, 'error');
165
+ // Phase 8: File Upload Testing (active probing)
166
+ if (allowActive) {
167
+ this.progress(phases[7].name, phases[7].label, (completedPhases / phases.length) * 100);
168
+ try {
169
+ const uploader = new FileUploadTester(logger);
170
+ const findings = await uploader.test(surfaceInventory);
171
+ this.addFindings(findings);
172
+ this._log(`File uploads: ${findings.length} issues`);
173
+ } catch (err) {
174
+ this._log(`File upload testing failed: ${err.message}`, 'error');
175
+ }
176
+ } else {
177
+ skipActive('File upload testing');
154
178
  }
155
179
  completedPhases++;
156
180
 
@@ -166,15 +190,19 @@ export class SecurityAgent extends BaseAgent {
166
190
  }
167
191
  completedPhases++;
168
192
 
169
- // Phase 10: Open Redirect Detection
170
- this.progress(phases[9].name, phases[9].label, (completedPhases / phases.length) * 100);
171
- try {
172
- const detector = new OpenRedirectDetector(logger);
173
- const findings = await detector.detect(surfaceInventory);
174
- this.addFindings(findings);
175
- this._log(`Open redirects: ${findings.length} issues`);
176
- } catch (err) {
177
- this._log(`Open redirect detection failed: ${err.message}`, 'error');
193
+ // Phase 10: Open Redirect Detection (active probing)
194
+ if (allowActive) {
195
+ this.progress(phases[9].name, phases[9].label, (completedPhases / phases.length) * 100);
196
+ try {
197
+ const detector = new OpenRedirectDetector(logger);
198
+ const findings = await detector.detect(surfaceInventory);
199
+ this.addFindings(findings);
200
+ this._log(`Open redirects: ${findings.length} issues`);
201
+ } catch (err) {
202
+ this._log(`Open redirect detection failed: ${err.message}`, 'error');
203
+ }
204
+ } else {
205
+ skipActive('Open redirect detection');
178
206
  }
179
207
  completedPhases++;
180
208
 
@@ -226,15 +254,19 @@ export class SecurityAgent extends BaseAgent {
226
254
  }
227
255
  completedPhases++;
228
256
 
229
- // Phase 15: SSRF Probing
230
- this.progress(phases[14].name, phases[14].label, (completedPhases / phases.length) * 100);
231
- try {
232
- const prober = new SSRFProber(logger);
233
- const findings = await prober.probe(surfaceInventory);
234
- this.addFindings(findings);
235
- this._log(`SSRF: ${findings.length} issues`);
236
- } catch (err) {
237
- this._log(`SSRF probing failed: ${err.message}`, 'error');
257
+ // Phase 15: SSRF Probing (active probing)
258
+ if (allowActive) {
259
+ this.progress(phases[14].name, phases[14].label, (completedPhases / phases.length) * 100);
260
+ try {
261
+ const prober = new SSRFProber(logger);
262
+ const findings = await prober.probe(surfaceInventory);
263
+ this.addFindings(findings);
264
+ this._log(`SSRF: ${findings.length} issues`);
265
+ } catch (err) {
266
+ this._log(`SSRF probing failed: ${err.message}`, 'error');
267
+ }
268
+ } else {
269
+ skipActive('SSRF probing');
238
270
  }
239
271
  completedPhases++;
240
272