pentesting 0.49.2 → 0.49.3

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 (2) hide show
  1. package/dist/main.js +503 -223
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -331,7 +331,7 @@ var ORPHAN_PROCESS_NAMES = [
331
331
 
332
332
  // src/shared/constants/agent.ts
333
333
  var APP_NAME = "Pentest AI";
334
- var APP_VERSION = "0.49.2";
334
+ var APP_VERSION = "0.49.3";
335
335
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
336
336
  var LLM_ROLES = {
337
337
  SYSTEM: "system",
@@ -704,6 +704,16 @@ var ATTACK_VALUE_RANK = {
704
704
  LOW: 1,
705
705
  NONE: 0
706
706
  };
707
+ var CONFIDENCE_THRESHOLDS = {
708
+ /** ≥80: exploit confirmed, shell access, flag captured */
709
+ CONFIRMED: 80,
710
+ /** ≥50: strong but not yet proven */
711
+ PROBABLE: 50,
712
+ /** ≥25: initial discovery, weak signal */
713
+ POSSIBLE: 25,
714
+ /** <25: speculation only */
715
+ NONE: 0
716
+ };
707
717
  var APPROVAL_STATUSES = {
708
718
  AUTO: "auto",
709
719
  USER_CONFIRMED: "user_confirmed",
@@ -794,7 +804,11 @@ var DEFAULTS = {
794
804
  ENGAGEMENT_NAME: "auto-assessment",
795
805
  ENGAGEMENT_CLIENT: "internal",
796
806
  UNKNOWN_PHASE: "unknown",
797
- INIT_PHASE: "init"
807
+ INIT_PHASE: "init",
808
+ /** Fallback service name when port fingerprinting yielded no result */
809
+ UNKNOWN_SERVICE: "unknown",
810
+ /** Default port state when not explicitly specified */
811
+ PORT_STATE_OPEN: "open"
798
812
  };
799
813
 
800
814
  // src/engine/process-manager.ts
@@ -2460,17 +2474,16 @@ var StateSerializer = class {
2460
2474
  }
2461
2475
  const findings = state.getFindings();
2462
2476
  if (findings.length > 0) {
2463
- const counts = findings.reduce((acc, f) => {
2464
- acc[f.severity] = (acc[f.severity] || 0) + 1;
2465
- return acc;
2466
- }, { critical: 0, high: 0, medium: 0, low: 0 });
2467
- lines.push(`Findings: ${findings.length} total (crit:${counts.critical} high:${counts.high} med:${counts.medium})`);
2468
- const important = findings.filter((f) => f.severity === "critical" || f.severity === "high");
2469
- if (important.length > 0) {
2470
- lines.push(` Important Findings:`);
2471
- for (const f of important.slice(0, DISPLAY_LIMITS.FINDING_PREVIEW)) {
2477
+ const confirmed = findings.filter((f) => f.confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED).length;
2478
+ const probable = findings.filter((f) => f.confidence >= CONFIDENCE_THRESHOLDS.PROBABLE && f.confidence < CONFIDENCE_THRESHOLDS.CONFIRMED).length;
2479
+ const possible = findings.filter((f) => f.confidence < CONFIDENCE_THRESHOLDS.PROBABLE).length;
2480
+ lines.push(`Findings: ${findings.length} total (confirmed:${confirmed} probable:${probable} possible:${possible})`);
2481
+ const highPriority = findings.filter((f) => f.confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED).sort((a, b) => b.confidence - a.confidence);
2482
+ if (highPriority.length > 0) {
2483
+ lines.push(` Confirmed Findings (\u2265${CONFIDENCE_THRESHOLDS.CONFIRMED}):`);
2484
+ for (const f of highPriority.slice(0, DISPLAY_LIMITS.FINDING_PREVIEW)) {
2472
2485
  const tactic = f.attackPattern ? ` [ATT&CK:${f.attackPattern}]` : "";
2473
- lines.push(` [${f.severity.toUpperCase()}] ${f.title} (${f.category || "general"})${tactic}`);
2486
+ lines.push(` [conf:${f.confidence}|${f.severity.toUpperCase()}] ${f.title} (${f.category || "general"})${tactic}`);
2474
2487
  }
2475
2488
  }
2476
2489
  }
@@ -3350,7 +3363,7 @@ var DynamicTechniqueLibrary = class {
3350
3363
  });
3351
3364
  if (this.techniques.length > this.maxTechniques) {
3352
3365
  this.techniques.sort((a, b) => {
3353
- if (a.isVerified !== b.isVerified) return a.isVerified ? -1 : 1;
3366
+ if (a.confidence !== b.confidence) return b.confidence - a.confidence;
3354
3367
  return b.learnedAt - a.learnedAt;
3355
3368
  });
3356
3369
  this.techniques = this.techniques.slice(0, this.maxTechniques);
@@ -3386,18 +3399,20 @@ var DynamicTechniqueLibrary = class {
3386
3399
  source: `Web search: "${query}"`,
3387
3400
  technique: tech,
3388
3401
  applicableTo,
3389
- isVerified: false,
3402
+ confidence: CONFIDENCE_THRESHOLDS.POSSIBLE,
3403
+ // discovered, not yet tested
3390
3404
  fromQuery: query
3391
3405
  });
3392
3406
  }
3393
3407
  }
3394
3408
  /**
3395
3409
  * Mark a technique as verified (it worked in practice).
3410
+ * Upgrades confidence to 80.
3396
3411
  */
3397
3412
  verify(techniqueSubstring) {
3398
3413
  for (const t of this.techniques) {
3399
3414
  if (t.technique.toLowerCase().includes(techniqueSubstring.toLowerCase())) {
3400
- t.isVerified = true;
3415
+ t.confidence = CONFIDENCE_THRESHOLDS.CONFIRMED;
3401
3416
  }
3402
3417
  }
3403
3418
  }
@@ -3425,18 +3440,18 @@ var DynamicTechniqueLibrary = class {
3425
3440
  */
3426
3441
  toPrompt() {
3427
3442
  if (this.techniques.length === 0) return "";
3428
- const verified = this.techniques.filter((t) => t.isVerified);
3429
- const unverified = this.techniques.filter((t) => !t.isVerified);
3443
+ const confirmed = this.techniques.filter((t) => t.confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED);
3444
+ const discovered = this.techniques.filter((t) => t.confidence < CONFIDENCE_THRESHOLDS.CONFIRMED);
3430
3445
  const lines = ["<learned-techniques>"];
3431
- if (verified.length > 0) {
3432
- lines.push("VERIFIED (worked in this session):");
3433
- for (const t of verified) {
3446
+ if (confirmed.length > 0) {
3447
+ lines.push("CONFIRMED (worked in this session):");
3448
+ for (const t of confirmed) {
3434
3449
  lines.push(` \u2705 [${t.applicableTo.join(",")}] ${t.technique}`);
3435
3450
  }
3436
3451
  }
3437
- if (unverified.length > 0) {
3438
- lines.push(`DISCOVERED (${unverified.length} unverified):`);
3439
- for (const t of unverified.slice(0, MEMORY_LIMITS.PROMPT_UNVERIFIED_TECHNIQUES)) {
3452
+ if (discovered.length > 0) {
3453
+ lines.push(`DISCOVERED (${discovered.length} unverified):`);
3454
+ for (const t of discovered.slice(0, MEMORY_LIMITS.PROMPT_UNVERIFIED_TECHNIQUES)) {
3440
3455
  lines.push(` \u{1F4A1} [${t.applicableTo.join(",")}] ${t.technique} (from: ${t.source})`);
3441
3456
  }
3442
3457
  }
@@ -3659,6 +3674,14 @@ var SharedState = class {
3659
3674
  getFindingsBySeverity(severity) {
3660
3675
  return this.data.findings.filter((f) => f.severity === severity);
3661
3676
  }
3677
+ /** Returns findings with confidence >= threshold (default: CONFIRMED = 80) */
3678
+ getFindingsByConfidence(threshold = CONFIDENCE_THRESHOLDS.CONFIRMED) {
3679
+ return this.data.findings.filter((f) => f.confidence >= threshold);
3680
+ }
3681
+ /** True if confidence >= CONFIRMED (80) */
3682
+ isConfirmedFinding(finding) {
3683
+ return finding.confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED;
3684
+ }
3662
3685
  addLoot(loot) {
3663
3686
  this.data.loot.push(loot);
3664
3687
  }
@@ -5044,43 +5067,32 @@ Reason: ${reason}`
5044
5067
  ];
5045
5068
 
5046
5069
  // src/shared/utils/finding-validator.ts
5047
- var VALIDATION_THRESHOLDS = {
5048
- /** Divisor for base confidence (N+ pattern matches = 100%) */
5049
- CONFIDENCE_DIVISOR: 2,
5050
- /** Penalty per false-positive indicator */
5051
- FALSE_POSITIVE_PENALTY: 0.15,
5052
- /** Confidence breakpoints for quality classification */
5053
- QUALITY_STRONG: 0.8,
5054
- QUALITY_MODERATE: 0.5,
5055
- /** Minimum confidence for verification */
5056
- VERIFICATION_MIN: 0.5
5057
- };
5058
5070
  var SUCCESS_PATTERNS = [
5059
- // Shell access indicators
5060
- { pattern: /uid=\d+\([^)]+\)\s+gid=\d+/, description: "Unix id output", weight: 1 },
5061
- { pattern: /root:x:0:0/, description: "/etc/passwd root entry", weight: 0.9 },
5062
- { pattern: /NT AUTHORITY\\SYSTEM/i, description: "Windows SYSTEM access", weight: 1 },
5063
- { pattern: /nt authority\\system/i, description: "Windows SYSTEM access (lowercase)", weight: 1 },
5064
- { pattern: /\$ whoami\s*\n\s*root/, description: "root whoami output", weight: 1 },
5065
- { pattern: /Administrator/, description: "Windows Administrator", weight: 0.7 },
5066
- // Database access
5067
- { pattern: /\d+ rows? in set/, description: "SQL query result", weight: 0.8 },
5068
- { pattern: /mysql>|postgres[=#]|sqlite>/, description: "Database shell prompt", weight: 0.9 },
5069
- { pattern: /CREATE TABLE|INSERT INTO|SELECT \*/i, description: "SQL DDL/DML output", weight: 0.7 },
5070
- // File read success
5071
- { pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/, description: "Private key exposure", weight: 1 },
5072
- { pattern: /-----BEGIN CERTIFICATE-----/, description: "Certificate exposure", weight: 0.6 },
5073
- { pattern: /DB_PASSWORD|DATABASE_URL|SECRET_KEY/i, description: "Credential in config", weight: 0.8 },
5074
- // RCE indicators
5075
- { pattern: /Linux\s+\S+\s+\d+\.\d+/, description: "Linux uname output", weight: 0.7 },
5076
- { pattern: /Windows\s+(Server\s+)?\d{4}/i, description: "Windows systeminfo", weight: 0.7 },
5077
- { pattern: /\bwww-data\b/, description: "Web server user context", weight: 0.6 },
5078
- // Network access
5079
- { pattern: /Nmap scan report for/, description: "Internal nmap scan (pivoting)", weight: 0.5 },
5080
- { pattern: /meterpreter\s*>/, description: "Meterpreter session", weight: 1 },
5081
- // Credential extraction
5082
- { pattern: /\b[a-f0-9]{32}\b:\b[a-f0-9]{32}\b/, description: "Hash:hash pair", weight: 0.7 },
5083
- { pattern: /password\s*[:=]\s*\S+/i, description: "Password in output", weight: 0.6 }
5071
+ // Absolute proof (100)
5072
+ { pattern: /uid=0\([^)]+\)\s+gid=0/, description: "uid=0 (root shell confirmed)", score: 100 },
5073
+ { pattern: /NT AUTHORITY\\SYSTEM/i, description: "Windows SYSTEM access", score: 100 },
5074
+ { pattern: /meterpreter\s*>/, description: "Meterpreter session", score: 100 },
5075
+ // Shell / access confirmed (90-95)
5076
+ { pattern: /\$ whoami\s*\n\s*root/, description: "root whoami output", score: 95 },
5077
+ { pattern: /root:x:0:0/, description: "/etc/passwd root entry read", score: 90 },
5078
+ { pattern: /uid=\d+\([^)]+\)\s+gid=\d+/, description: "Unix id command output", score: 90 },
5079
+ // Credential / sensitive data extracted (80-85)
5080
+ { pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/, description: "Private key exposed", score: 85 },
5081
+ { pattern: /DB_PASSWORD|DATABASE_URL|SECRET_KEY/i, description: "Secret/credential in config", score: 80 },
5082
+ { pattern: /\d+ rows? in set/i, description: "SQL query result returned", score: 80 },
5083
+ { pattern: /mysql>|postgres[=#]|sqlite>/, description: "Database shell prompt", score: 80 },
5084
+ { pattern: /password\s*[:=]\s*\S+/i, description: "Password in output", score: 80 },
5085
+ { pattern: /\b[a-f0-9]{32}\b:\b[a-f0-9]{32}\b/, description: "Hash pair extracted", score: 80 },
5086
+ // Strong indicators (65-75)
5087
+ { pattern: /-----BEGIN CERTIFICATE-----/, description: "Certificate exposed", score: 70 },
5088
+ { pattern: /CREATE TABLE|INSERT INTO|SELECT \*/i, description: "SQL DDL/DML output", score: 70 },
5089
+ { pattern: /Administrator/i, description: "Windows Administrator context", score: 70 },
5090
+ { pattern: /Linux\s+\S+\s+\d+\.\d+/, description: "Linux uname output (RCE)", score: 65 },
5091
+ { pattern: /Windows\s+(Server\s+)?\d{4}/i, description: "Windows systeminfo (RCE)", score: 65 },
5092
+ { pattern: /\bwww-data\b/, description: "Web server user context", score: 65 },
5093
+ // Circumstantial evidence (50)
5094
+ { pattern: /Nmap scan report for/, description: "Internal nmap scan (pivot)", score: 50 },
5095
+ { pattern: /open\s+\w+\/\w+/, description: "Open port in scan output", score: 25 }
5084
5096
  ];
5085
5097
  var FALSE_POSITIVE_PATTERNS = [
5086
5098
  { pattern: /connection refused/i, description: "Connection refused" },
@@ -5089,66 +5101,83 @@ var FALSE_POSITIVE_PATTERNS = [
5089
5101
  { pattern: /404 not found/i, description: "404 response" },
5090
5102
  { pattern: /401 unauthorized/i, description: "Unauthorized" },
5091
5103
  { pattern: /timeout|timed out/i, description: "Timeout" },
5092
- { pattern: /error:|exception:/i, description: "Error/Exception" }
5104
+ { pattern: /error:|exception:/i, description: "Error/Exception header" }
5093
5105
  ];
5094
- function validateFinding(evidence, severity) {
5106
+ var FALSE_POSITIVE_PENALTY = 15;
5107
+ function validateFinding(evidence) {
5095
5108
  if (!evidence || evidence.length === 0) {
5096
5109
  return {
5097
- isVerified: false,
5098
5110
  confidence: 0,
5099
- verificationNote: "No evidence provided \u2014 finding is unverified.",
5100
- evidenceQuality: "none"
5111
+ evidenceQuality: "none",
5112
+ verificationNote: "No evidence provided \u2014 finding is unsubstantiated."
5101
5113
  };
5102
5114
  }
5103
- const combinedEvidence = evidence.join("\n");
5104
- const flags = detectFlags(combinedEvidence);
5115
+ const combined = evidence.join("\n");
5116
+ const flags = detectFlags(combined);
5105
5117
  if (flags.length > 0) {
5106
5118
  return {
5107
- isVerified: true,
5108
- confidence: 1,
5109
- verificationNote: `CTF flag detected in evidence: ${flags[0]}`,
5110
- evidenceQuality: "strong"
5119
+ confidence: 100,
5120
+ evidenceQuality: "confirmed",
5121
+ verificationNote: `CTF flag detected in evidence: ${flags[0]}`
5111
5122
  };
5112
5123
  }
5113
- let totalWeight = 0;
5114
- const matchedPatterns = [];
5115
- for (const { pattern, description, weight } of SUCCESS_PATTERNS) {
5124
+ let maxScore = 0;
5125
+ const matched = [];
5126
+ for (const { pattern, description, score } of SUCCESS_PATTERNS) {
5116
5127
  pattern.lastIndex = 0;
5117
- if (pattern.test(combinedEvidence)) {
5118
- totalWeight += weight;
5119
- matchedPatterns.push(description);
5128
+ if (pattern.test(combined)) {
5129
+ if (score > maxScore) maxScore = score;
5130
+ matched.push(`${description} (+${score})`);
5120
5131
  }
5121
5132
  }
5122
- let falsePositiveCount = 0;
5123
- for (const { pattern } of FALSE_POSITIVE_PATTERNS) {
5133
+ let fpCount = 0;
5134
+ const fpMatched = [];
5135
+ for (const { pattern, description } of FALSE_POSITIVE_PATTERNS) {
5124
5136
  pattern.lastIndex = 0;
5125
- if (pattern.test(combinedEvidence)) {
5126
- falsePositiveCount++;
5127
- }
5128
- }
5129
- const baseConfidence = Math.min(1, totalWeight / VALIDATION_THRESHOLDS.CONFIDENCE_DIVISOR);
5130
- const fpPenalty = falsePositiveCount * VALIDATION_THRESHOLDS.FALSE_POSITIVE_PENALTY;
5131
- const confidence = Math.max(0, baseConfidence - fpPenalty);
5132
- let evidenceQuality;
5133
- if (confidence >= VALIDATION_THRESHOLDS.QUALITY_STRONG) evidenceQuality = "strong";
5134
- else if (confidence >= VALIDATION_THRESHOLDS.QUALITY_MODERATE) evidenceQuality = "moderate";
5135
- else if (confidence > 0) evidenceQuality = "weak";
5136
- else evidenceQuality = "none";
5137
- const isVerified = confidence >= VALIDATION_THRESHOLDS.VERIFICATION_MIN;
5138
- const note = matchedPatterns.length > 0 ? `Evidence matches: ${matchedPatterns.join(", ")}. Confidence: ${(confidence * 100).toFixed(0)}%` : `No recognized success patterns in evidence. ${falsePositiveCount > 0 ? `${falsePositiveCount} potential false-positive indicators found.` : "Manual review recommended."}`;
5139
- return {
5140
- isVerified,
5141
- confidence,
5142
- verificationNote: note,
5143
- evidenceQuality
5144
- };
5137
+ if (pattern.test(combined)) {
5138
+ fpCount++;
5139
+ fpMatched.push(description);
5140
+ }
5141
+ }
5142
+ const raw = maxScore - fpCount * FALSE_POSITIVE_PENALTY;
5143
+ const confidence = Math.max(0, Math.min(100, Math.round(raw)));
5144
+ const evidenceQuality = qualityFromScore(confidence);
5145
+ const note = buildNote(matched, fpMatched, confidence);
5146
+ return { confidence, evidenceQuality, verificationNote: note };
5147
+ }
5148
+ function qualityFromScore(score) {
5149
+ if (score >= CONFIDENCE_THRESHOLDS.CONFIRMED) return "confirmed";
5150
+ if (score >= CONFIDENCE_THRESHOLDS.PROBABLE) return "probable";
5151
+ if (score >= CONFIDENCE_THRESHOLDS.POSSIBLE) return "possible";
5152
+ return "none";
5153
+ }
5154
+ function buildNote(matched, fpMatched, confidence) {
5155
+ const parts = [];
5156
+ if (matched.length > 0) {
5157
+ parts.push(`Matched: ${matched.join(", ")}`);
5158
+ }
5159
+ if (fpMatched.length > 0) {
5160
+ parts.push(`FP penalties (${fpMatched.length}\xD7): ${fpMatched.join(", ")}`);
5161
+ }
5162
+ if (parts.length === 0) {
5163
+ parts.push("No recognized success patterns");
5164
+ }
5165
+ parts.push(`Confidence: ${confidence}/100`);
5166
+ return parts.join(" | ");
5145
5167
  }
5146
5168
  function formatValidation(result2) {
5147
- const icon = result2.isVerified ? "\u2705" : "\u26A0\uFE0F";
5148
- return `${icon} Verified: ${result2.isVerified} | Quality: ${result2.evidenceQuality} | ${result2.verificationNote}`;
5169
+ const icon = result2.confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED ? "\u2705" : result2.confidence >= CONFIDENCE_THRESHOLDS.PROBABLE ? "\u{1F536}" : result2.confidence >= CONFIDENCE_THRESHOLDS.POSSIBLE ? "\u26A0\uFE0F" : "\u2753";
5170
+ return `${icon} [${result2.confidence}/100] ${result2.evidenceQuality.toUpperCase()} | ${result2.verificationNote}`;
5149
5171
  }
5150
5172
 
5151
5173
  // src/engine/tools/pentest-target-tools.ts
5174
+ var CRACKABLE_LOOT_TYPES = /* @__PURE__ */ new Set([LOOT_TYPES.HASH]);
5175
+ var SPRAY_LOOT_TYPES = /* @__PURE__ */ new Set([
5176
+ LOOT_TYPES.CREDENTIAL,
5177
+ LOOT_TYPES.TOKEN,
5178
+ LOOT_TYPES.SSH_KEY,
5179
+ LOOT_TYPES.API_KEY
5180
+ ]);
5152
5181
  function isPortArray(value) {
5153
5182
  if (!Array.isArray(value)) return false;
5154
5183
  return value.every(
@@ -5162,13 +5191,13 @@ function isValidSeverity(value) {
5162
5191
  return typeof value === "string" && Object.values(SEVERITIES).includes(value);
5163
5192
  }
5164
5193
  function parseSeverity(value) {
5165
- return isValidSeverity(value) ? value : "medium";
5194
+ return isValidSeverity(value) ? value : SEVERITIES.MEDIUM;
5166
5195
  }
5167
5196
  function isValidLootType(value) {
5168
5197
  return typeof value === "string" && Object.values(LOOT_TYPES).includes(value);
5169
5198
  }
5170
5199
  function parseLootType(value) {
5171
- return isValidLootType(value) ? value : "file";
5200
+ return isValidLootType(value) ? value : LOOT_TYPES.FILE;
5172
5201
  }
5173
5202
  function isValidAttackTactic(value) {
5174
5203
  return typeof value === "string" && Object.values(ATTACK_TACTICS).includes(value);
@@ -5219,12 +5248,12 @@ The target will be tracked in SharedState and available for all agents.`,
5219
5248
  if (!exists) {
5220
5249
  existing.ports.push({
5221
5250
  port: np.port,
5222
- service: np.service || "unknown",
5251
+ service: np.service || DEFAULTS.UNKNOWN_SERVICE,
5223
5252
  version: np.version,
5224
- state: np.state || "open",
5253
+ state: np.state || DEFAULTS.PORT_STATE_OPEN,
5225
5254
  notes: []
5226
5255
  });
5227
- state.attackGraph.addService(ip, np.port, np.service || "unknown", np.version);
5256
+ state.attackGraph.addService(ip, np.port, np.service || DEFAULTS.UNKNOWN_SERVICE, np.version);
5228
5257
  }
5229
5258
  }
5230
5259
  if (p.hostname) existing.hostname = parseString(p.hostname);
@@ -5233,9 +5262,9 @@ The target will be tracked in SharedState and available for all agents.`,
5233
5262
  }
5234
5263
  const ports = parsePorts(p.ports).map((port) => ({
5235
5264
  port: port.port,
5236
- service: port.service || "unknown",
5265
+ service: port.service || DEFAULTS.UNKNOWN_SERVICE,
5237
5266
  version: port.version,
5238
- state: port.state || "open",
5267
+ state: port.state || DEFAULTS.PORT_STATE_OPEN,
5239
5268
  notes: []
5240
5269
  }));
5241
5270
  state.addTarget({
@@ -5262,18 +5291,18 @@ Types: credential, hash, token, ssh_key, api_key, file, session, ticket, certifi
5262
5291
  required: ["type", "host", "detail"],
5263
5292
  execute: async (p) => {
5264
5293
  const lootTypeStr = parseString(p.type);
5265
- const crackableTypes = ["hash"];
5266
5294
  const detail = parseString(p.detail);
5267
5295
  const host = parseString(p.host);
5296
+ const isCrackable = CRACKABLE_LOOT_TYPES.has(lootTypeStr);
5268
5297
  state.addLoot({
5269
5298
  type: parseLootType(lootTypeStr),
5270
5299
  host,
5271
5300
  detail,
5272
5301
  obtainedAt: Date.now(),
5273
- isCrackable: crackableTypes.includes(lootTypeStr),
5302
+ isCrackable,
5274
5303
  isCracked: false
5275
5304
  });
5276
- if (["credential", "token", "ssh_key", "api_key"].includes(lootTypeStr)) {
5305
+ if (SPRAY_LOOT_TYPES.has(lootTypeStr)) {
5277
5306
  const parts = detail.split(":");
5278
5307
  if (parts.length >= 2) {
5279
5308
  state.attackGraph.addCredential(parts[0], parts.slice(1).join(":"), host);
@@ -5284,22 +5313,35 @@ Types: credential, hash, token, ssh_key, api_key, file, session, ticket, certifi
5284
5313
  success: true,
5285
5314
  output: `Loot recorded: [${lootTypeStr}] from ${host}
5286
5315
  Detail: ${detail}
5287
- ` + (crackableTypes.includes(lootTypeStr) ? `This is crackable. Consider: hash_crack({ hashes: "${detail.slice(0, DISPLAY_LIMITS.LOOT_DETAIL_PREVIEW)}..." })` : `Consider credential reuse / lateral movement with this loot.`)
5316
+ ` + (isCrackable ? `This is crackable. Consider: hash_crack({ hashes: "${detail.slice(0, DISPLAY_LIMITS.LOOT_DETAIL_PREVIEW)}..." })` : `Consider credential reuse / lateral movement with this loot.`)
5288
5317
  };
5289
5318
  }
5290
5319
  },
5291
5320
  {
5292
5321
  name: TOOL_NAMES.ADD_FINDING,
5293
- description: `Add a security finding with full details.
5294
- ALWAYS provide: description (HOW you exploited it, step-by-step), evidence (actual command output proving success), and attackPattern (MITRE ATT&CK tactic).
5295
- Findings without evidence are marked as UNVERIFIED and have low credibility.`,
5322
+ description: `Record a security finding with confidence score.
5323
+
5324
+ ALWAYS provide: description (HOW exploited), evidence (actual command output), attackPattern (MITRE ATT&CK tactic).
5325
+
5326
+ confidence score (0-100) \u2014 technical verification level:
5327
+ 100 = CTF flag captured, root shell confirmed (uid=0), NT AUTHORITY\\SYSTEM
5328
+ 80 = DB query result, private key read, auth bypass proven, credential extracted
5329
+ 75 = Stack trace / internal paths / suspicious error message
5330
+ 65 = RCE circumstantial (uname output, www-data context)
5331
+ 50 = CVE version match, unusual server response
5332
+ 25 = Port open, service detected (unverified \u2014 needs further testing)
5333
+ 0 = Pure speculation, no actual test performed
5334
+
5335
+ Omit confidence to let the system auto-calculate from evidence.
5336
+ Findings with confidence >= 80 appear as CONFIRMED in reports.`,
5296
5337
  parameters: {
5297
- title: { type: "string", description: 'Concise finding title (e.g., "Path Traversal via /download endpoint")' },
5298
- severity: { type: "string", description: "Severity: critical, high, medium, low, info" },
5299
- affected: { type: "array", items: { type: "string" }, description: 'Affected host:port or URLs (e.g., ["ctf.example.com:443/download"])' },
5300
- description: { type: "string", description: "Detailed description: what the vulnerability is, how you exploited it step-by-step, what access it gives, and the impact. Include credentials found, methods used, and exploitation chain." },
5301
- evidence: { type: "array", items: { type: "string" }, description: 'Actual command outputs proving the finding (e.g., ["curl output showing /etc/passwd", "uid=0(root) gid=0(root)"]). Copy real output here.' },
5302
- attackPattern: { type: "string", description: "MITRE ATT&CK tactic: initial_access, execution, persistence, privilege_escalation, defense_evasion, credential_access, discovery, lateral_movement, collection, exfiltration, command_and_control, impact" }
5338
+ title: { type: "string", description: 'Concise title (e.g., "Path Traversal via /download endpoint")' },
5339
+ severity: { type: "string", description: "Business impact severity: critical, high, medium, low, info" },
5340
+ affected: { type: "array", items: { type: "string" }, description: "Affected host:port or URLs" },
5341
+ description: { type: "string", description: "What the vulnerability is, how you exploited it step-by-step, what access it gives, and the impact." },
5342
+ evidence: { type: "array", items: { type: "string" }, description: "Actual command outputs proving the finding. Copy real output here." },
5343
+ attackPattern: { type: "string", description: "MITRE ATT&CK tactic: initial_access, execution, persistence, privilege_escalation, defense_evasion, credential_access, discovery, lateral_movement, collection, exfiltration, command_and_control, impact" },
5344
+ confidence: { type: "number", description: "Optional override (0-100). Omit to auto-calculate from evidence." }
5303
5345
  },
5304
5346
  required: ["title", "severity", "description", "evidence"],
5305
5347
  execute: async (p) => {
@@ -5308,31 +5350,34 @@ Findings without evidence are marked as UNVERIFIED and have low credibility.`,
5308
5350
  const severity = parseSeverity(p.severity);
5309
5351
  const affected = parseStringArray(p.affected);
5310
5352
  const description = parseString(p.description);
5311
- const validation = validateFinding(evidence, severity);
5312
5353
  const attackPattern = parseString(p.attackPattern);
5354
+ const validation = validateFinding(evidence);
5355
+ const rawOverride = p.confidence;
5356
+ const confidence = typeof rawOverride === "number" && rawOverride >= 0 && rawOverride <= 100 ? Math.round(rawOverride) : validation.confidence;
5313
5357
  state.addFinding({
5314
5358
  id: generateId(AGENT_LIMITS.ID_RADIX, AGENT_LIMITS.ID_LENGTH),
5315
5359
  title,
5316
5360
  severity,
5361
+ confidence,
5317
5362
  affected,
5318
5363
  description,
5319
5364
  evidence,
5320
- isVerified: validation.isVerified,
5321
5365
  remediation: "",
5322
5366
  foundAt: Date.now(),
5323
5367
  ...attackPattern && isValidAttackTactic(attackPattern) ? { attackPattern } : {}
5324
5368
  });
5325
- const hasExploit = validation.isVerified;
5326
- const target = affected[0] || "unknown";
5369
+ const hasExploit = confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED;
5370
+ const target = affected[0] || DEFAULTS.UNKNOWN_SERVICE;
5327
5371
  state.attackGraph.addVulnerability(title, target, severity, hasExploit);
5372
+ const memoryEvent = confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED ? "tool_success" : "tool_failure";
5328
5373
  state.episodicMemory.record(
5329
- validation.isVerified ? "tool_success" : "tool_failure",
5374
+ memoryEvent,
5330
5375
  `Finding: ${title} (${severity}) \u2014 ${formatValidation(validation)}`
5331
5376
  );
5332
5377
  return {
5333
5378
  success: true,
5334
5379
  output: `Added: ${title}
5335
- ${formatValidation(validation)}`
5380
+ ${formatValidation(validation)}${rawOverride !== void 0 ? ` [confidence overridden to ${confidence}]` : ""}`
5336
5381
  };
5337
5382
  }
5338
5383
  }
@@ -5385,6 +5430,7 @@ function isBrowserHeadless() {
5385
5430
  var SEARCH_URL_PATTERN = {
5386
5431
  GLM: "bigmodel.cn",
5387
5432
  ZHIPU: "zhipuai",
5433
+ Z_AI: "z.ai",
5388
5434
  BRAVE: "brave.com",
5389
5435
  SERPER: "serper.dev"
5390
5436
  };
@@ -6211,7 +6257,7 @@ async function webSearch(query, _engine) {
6211
6257
  };
6212
6258
  }
6213
6259
  try {
6214
- if (apiUrl.includes(SEARCH_URL_PATTERN.GLM) || apiUrl.includes(SEARCH_URL_PATTERN.ZHIPU)) {
6260
+ if (apiUrl.includes(SEARCH_URL_PATTERN.GLM) || apiUrl.includes(SEARCH_URL_PATTERN.ZHIPU) || apiUrl.includes(SEARCH_URL_PATTERN.Z_AI)) {
6215
6261
  debugLog("search", "Using GLM search");
6216
6262
  return await searchWithGLM(query, apiKey, apiUrl);
6217
6263
  } else if (apiUrl.includes(SEARCH_URL_PATTERN.BRAVE)) {
@@ -7164,7 +7210,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
7164
7210
  },
7165
7211
  execute: async (p) => {
7166
7212
  const { existsSync: existsSync12, statSync: statSync3, readdirSync: readdirSync4 } = await import("fs");
7167
- const { join: join14 } = await import("path");
7213
+ const { join: join13 } = await import("path");
7168
7214
  const category = p.category || "";
7169
7215
  const search = p.search || "";
7170
7216
  const minSize = p.min_size || 0;
@@ -7219,7 +7265,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
7219
7265
  }
7220
7266
  for (const entry of entries) {
7221
7267
  if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
7222
- const fullPath = join14(dirPath, entry.name);
7268
+ const fullPath = join13(dirPath, entry.name);
7223
7269
  if (entry.isDirectory()) {
7224
7270
  scanDir(fullPath, maxDepth, depth + 1);
7225
7271
  continue;
@@ -9686,15 +9732,9 @@ function logLLM(message, data) {
9686
9732
  debugLog("llm", message, data);
9687
9733
  }
9688
9734
 
9689
- // src/engine/orchestrator/orchestrator.ts
9690
- import { fileURLToPath as fileURLToPath2 } from "url";
9691
- import { dirname as dirname4, join as join8 } from "path";
9692
- var __filename = fileURLToPath2(import.meta.url);
9693
- var __dirname2 = dirname4(__filename);
9694
-
9695
9735
  // src/engine/state-persistence.ts
9696
9736
  import { writeFileSync as writeFileSync6, readFileSync as readFileSync4, existsSync as existsSync6, readdirSync, statSync, unlinkSync as unlinkSync4, rmSync } from "fs";
9697
- import { join as join9 } from "path";
9737
+ import { join as join8 } from "path";
9698
9738
  function saveState(state) {
9699
9739
  const sessionsDir = WORKSPACE.SESSIONS;
9700
9740
  ensureDirExists(sessionsDir);
@@ -9712,9 +9752,9 @@ function saveState(state) {
9712
9752
  missionChecklist: state.getMissionChecklist()
9713
9753
  };
9714
9754
  const sessionId = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9715
- const sessionFile = join9(sessionsDir, `${sessionId}.json`);
9755
+ const sessionFile = join8(sessionsDir, `${sessionId}.json`);
9716
9756
  writeFileSync6(sessionFile, JSON.stringify(snapshot, null, 2), "utf-8");
9717
- const latestFile = join9(sessionsDir, "latest.json");
9757
+ const latestFile = join8(sessionsDir, "latest.json");
9718
9758
  writeFileSync6(latestFile, JSON.stringify(snapshot, null, 2), "utf-8");
9719
9759
  pruneOldSessions(sessionsDir);
9720
9760
  return sessionFile;
@@ -9723,8 +9763,8 @@ function pruneOldSessions(sessionsDir) {
9723
9763
  try {
9724
9764
  const sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(FILE_EXTENSIONS.JSON) && f !== SPECIAL_FILES.LATEST_STATE).map((f) => ({
9725
9765
  name: f,
9726
- path: join9(sessionsDir, f),
9727
- mtime: statSync(join9(sessionsDir, f)).mtimeMs
9766
+ path: join8(sessionsDir, f),
9767
+ mtime: statSync(join8(sessionsDir, f)).mtimeMs
9728
9768
  })).sort((a, b) => b.mtime - a.mtime);
9729
9769
  const toDelete = sessionFiles.slice(AGENT_LIMITS.MAX_SESSION_FILES);
9730
9770
  for (const file of toDelete) {
@@ -9734,7 +9774,7 @@ function pruneOldSessions(sessionsDir) {
9734
9774
  }
9735
9775
  }
9736
9776
  function loadState(state) {
9737
- const latestFile = join9(WORKSPACE.SESSIONS, "latest.json");
9777
+ const latestFile = join8(WORKSPACE.SESSIONS, "latest.json");
9738
9778
  if (!existsSync6(latestFile)) {
9739
9779
  return false;
9740
9780
  }
@@ -9752,7 +9792,11 @@ function loadState(state) {
9752
9792
  state.addTarget(value);
9753
9793
  }
9754
9794
  for (const finding of snapshot.findings) {
9755
- state.addFinding(finding);
9795
+ const legacyFinding = finding;
9796
+ if (typeof legacyFinding.confidence !== "number") {
9797
+ legacyFinding.confidence = legacyFinding.isVerified === true ? 80 : 25;
9798
+ }
9799
+ state.addFinding(legacyFinding);
9756
9800
  }
9757
9801
  for (const loot of snapshot.loot) {
9758
9802
  state.addLoot(loot);
@@ -10780,10 +10824,11 @@ RULES:
10780
10824
  id: generateId(),
10781
10825
  title,
10782
10826
  severity: "high",
10827
+ // Auto-extracted findings are unverified signals — score POSSIBLE (25)
10828
+ confidence: CONFIDENCE_THRESHOLDS.POSSIBLE,
10783
10829
  affected: [],
10784
10830
  description: `Auto-extracted by Analyst LLM: ${vector}`,
10785
10831
  evidence: digestResult.memo.keyFindings.slice(0, 5),
10786
- isVerified: false,
10787
10832
  remediation: "",
10788
10833
  foundAt: Date.now()
10789
10834
  });
@@ -10863,8 +10908,8 @@ RULES:
10863
10908
 
10864
10909
  // src/agents/prompt-builder.ts
10865
10910
  import { readFileSync as readFileSync6, existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
10866
- import { join as join11, dirname as dirname5 } from "path";
10867
- import { fileURLToPath as fileURLToPath3 } from "url";
10911
+ import { join as join10, dirname as dirname4 } from "path";
10912
+ import { fileURLToPath as fileURLToPath2 } from "url";
10868
10913
 
10869
10914
  // src/shared/constants/prompts.ts
10870
10915
  var PROMPT_PATHS = {
@@ -10909,13 +10954,27 @@ var PROMPT_DEFAULTS = {
10909
10954
  NO_SCOPE: "<scope>NO SCOPE DEFINED. STOP.</scope>",
10910
10955
  EMPTY_TODO: "Create initial plan",
10911
10956
  USER_CONTEXT: (context) => `
10912
- =========================================
10913
- \u{1F6A8} CRITICAL: USER INPUT (YOUR OBJECTIVE) \u{1F6A8}
10914
- =========================================
10957
+ <user-input>
10915
10958
  "${context}"
10959
+ </user-input>
10960
+
10961
+ <intent-rules>
10962
+ ANALYZE the user's intent before acting. Classify into ONE:
10963
+ ABORT \u2192 stop current work, confirm with \`ask_user\`
10964
+ CORRECTION \u2192 adjust approach, continue
10965
+ INFORMATION \u2192 store and USE immediately (credentials, paths, hints)
10966
+ COMMAND \u2192 execute EXACTLY what was asked, nothing more
10967
+ TARGET_CHANGE \u2192 \`add_target\`, then begin testing
10968
+ GUIDANCE \u2192 acknowledge via \`ask_user\`, adjust strategy, continue
10969
+ STATUS_QUERY \u2192 report via \`ask_user\`, then RESUME previous work
10970
+ CONVERSATION \u2192 respond via \`ask_user\`, do NOT scan or attack
10916
10971
 
10917
- RULE: If the user is just saying hello, asking a question, or did NOT provide a target, use the \`ask_user\` tool to respond and ask for a target. Do NOT start scanning unless a target is explicitly provided.
10918
- =========================================`
10972
+ RULES:
10973
+ - No target set and none provided \u2192 \`ask_user\` to request target.
10974
+ - Conversation or greeting \u2192 respond conversationally, do NOT attack.
10975
+ - Uncertain intent \u2192 ask for clarification with \`ask_user\`.
10976
+ - This is a collaborative tool. The user is your partner.
10977
+ </intent-rules>`
10919
10978
  };
10920
10979
  var PROMPT_CONFIG = {
10921
10980
  ENCODING: "utf-8"
@@ -11124,7 +11183,7 @@ function getAttacksForService(service, port) {
11124
11183
 
11125
11184
  // src/shared/utils/journal.ts
11126
11185
  import { writeFileSync as writeFileSync8, readFileSync as readFileSync5, existsSync as existsSync8, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync5 } from "fs";
11127
- import { join as join10 } from "path";
11186
+ import { join as join9 } from "path";
11128
11187
  var MAX_JOURNAL_ENTRIES = 50;
11129
11188
  var MAX_OUTPUT_FILES = 30;
11130
11189
  var TURN_PREFIX = "turn-";
@@ -11134,7 +11193,7 @@ function writeJournalEntry(entry) {
11134
11193
  const journalDir = WORKSPACE.JOURNAL;
11135
11194
  ensureDirExists(journalDir);
11136
11195
  const padded = String(entry.turn).padStart(4, "0");
11137
- const filePath = join10(journalDir, `${TURN_PREFIX}${padded}.json`);
11196
+ const filePath = join9(journalDir, `${TURN_PREFIX}${padded}.json`);
11138
11197
  writeFileSync8(filePath, JSON.stringify(entry, null, 2), "utf-8");
11139
11198
  return filePath;
11140
11199
  } catch (err) {
@@ -11144,7 +11203,7 @@ function writeJournalEntry(entry) {
11144
11203
  }
11145
11204
  function readJournalSummary() {
11146
11205
  try {
11147
- const summaryPath = join10(WORKSPACE.JOURNAL, SUMMARY_FILE);
11206
+ const summaryPath = join9(WORKSPACE.JOURNAL, SUMMARY_FILE);
11148
11207
  if (!existsSync8(summaryPath)) return "";
11149
11208
  return readFileSync5(summaryPath, "utf-8");
11150
11209
  } catch {
@@ -11159,7 +11218,7 @@ function getRecentEntries(count = MAX_JOURNAL_ENTRIES) {
11159
11218
  const entries = [];
11160
11219
  for (const file of files) {
11161
11220
  try {
11162
- const raw = readFileSync5(join10(journalDir, file), "utf-8");
11221
+ const raw = readFileSync5(join9(journalDir, file), "utf-8");
11163
11222
  entries.push(JSON.parse(raw));
11164
11223
  } catch {
11165
11224
  }
@@ -11189,7 +11248,7 @@ function regenerateJournalSummary() {
11189
11248
  const journalDir = WORKSPACE.JOURNAL;
11190
11249
  ensureDirExists(journalDir);
11191
11250
  const summary = buildSummaryFromEntries(entries);
11192
- const summaryPath = join10(journalDir, SUMMARY_FILE);
11251
+ const summaryPath = join9(journalDir, SUMMARY_FILE);
11193
11252
  writeFileSync8(summaryPath, summary, "utf-8");
11194
11253
  debugLog("general", "Journal summary regenerated", {
11195
11254
  entries: entries.length,
@@ -11301,7 +11360,7 @@ function rotateJournalEntries() {
11301
11360
  const toDelete = files.slice(0, files.length - MAX_JOURNAL_ENTRIES);
11302
11361
  for (const file of toDelete) {
11303
11362
  try {
11304
- unlinkSync5(join10(journalDir, file));
11363
+ unlinkSync5(join9(journalDir, file));
11305
11364
  } catch {
11306
11365
  }
11307
11366
  }
@@ -11318,8 +11377,8 @@ function rotateOutputFiles() {
11318
11377
  if (!existsSync8(outputDir)) return;
11319
11378
  const files = readdirSync2(outputDir).filter((f) => f.endsWith(".txt")).map((f) => ({
11320
11379
  name: f,
11321
- path: join10(outputDir, f),
11322
- mtime: statSync2(join10(outputDir, f)).mtimeMs
11380
+ path: join9(outputDir, f),
11381
+ mtime: statSync2(join9(outputDir, f)).mtimeMs
11323
11382
  })).sort((a, b) => b.mtime - a.mtime);
11324
11383
  if (files.length <= MAX_OUTPUT_FILES) return;
11325
11384
  const toDelete = files.slice(MAX_OUTPUT_FILES);
@@ -11345,7 +11404,7 @@ function rotateTurnRecords() {
11345
11404
  const toDelete = files.slice(0, files.length - MAX_JOURNAL_ENTRIES);
11346
11405
  for (const file of toDelete) {
11347
11406
  try {
11348
- unlinkSync5(join10(turnsDir, file));
11407
+ unlinkSync5(join9(turnsDir, file));
11349
11408
  } catch {
11350
11409
  }
11351
11410
  }
@@ -11358,9 +11417,9 @@ function rotateTurnRecords() {
11358
11417
  }
11359
11418
 
11360
11419
  // src/agents/prompt-builder.ts
11361
- var __dirname3 = dirname5(fileURLToPath3(import.meta.url));
11362
- var PROMPTS_DIR = join11(__dirname3, "prompts");
11363
- var TECHNIQUES_DIR = join11(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
11420
+ var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
11421
+ var PROMPTS_DIR = join10(__dirname2, "prompts");
11422
+ var TECHNIQUES_DIR = join10(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
11364
11423
  var { AGENT_FILES } = PROMPT_PATHS;
11365
11424
  var PHASE_PROMPT_MAP = {
11366
11425
  // Direct mappings — phase has its own prompt file
@@ -11493,7 +11552,7 @@ ${content}
11493
11552
  * Load a prompt file from src/agents/prompts/
11494
11553
  */
11495
11554
  loadPromptFile(filename) {
11496
- const path2 = join11(PROMPTS_DIR, filename);
11555
+ const path2 = join10(PROMPTS_DIR, filename);
11497
11556
  return existsSync9(path2) ? readFileSync6(path2, PROMPT_CONFIG.ENCODING) : "";
11498
11557
  }
11499
11558
  /**
@@ -11545,7 +11604,7 @@ ${content}
11545
11604
  const loadedSet = /* @__PURE__ */ new Set();
11546
11605
  const fragments = [];
11547
11606
  for (const technique of priorityTechniques) {
11548
- const filePath = join11(TECHNIQUES_DIR, `${technique}.md`);
11607
+ const filePath = join10(TECHNIQUES_DIR, `${technique}.md`);
11549
11608
  try {
11550
11609
  if (!existsSync9(filePath)) continue;
11551
11610
  const content = readFileSync6(filePath, PROMPT_CONFIG.ENCODING);
@@ -11561,7 +11620,7 @@ ${content}
11561
11620
  try {
11562
11621
  const allFiles = readdirSync3(TECHNIQUES_DIR).filter((f) => f.endsWith(".md") && f !== "README.md" && !loadedSet.has(f));
11563
11622
  for (const file of allFiles) {
11564
- const filePath = join11(TECHNIQUES_DIR, file);
11623
+ const filePath = join10(TECHNIQUES_DIR, file);
11565
11624
  const content = readFileSync6(filePath, PROMPT_CONFIG.ENCODING);
11566
11625
  if (content) {
11567
11626
  const category = file.replace(".md", "");
@@ -11672,7 +11731,7 @@ ${lines.join("\n")}
11672
11731
  */
11673
11732
  getJournalFragment() {
11674
11733
  try {
11675
- const summaryPath = join11(WORKSPACE.TURNS, "summary.md");
11734
+ const summaryPath = join10(WORKSPACE.TURNS, "summary.md");
11676
11735
  if (existsSync9(summaryPath)) {
11677
11736
  const summary2 = readFileSync6(summaryPath, "utf-8");
11678
11737
  if (summary2.trim()) {
@@ -11703,10 +11762,10 @@ ${summary}
11703
11762
 
11704
11763
  // src/agents/strategist.ts
11705
11764
  import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
11706
- import { join as join12, dirname as dirname6 } from "path";
11707
- import { fileURLToPath as fileURLToPath4 } from "url";
11708
- var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
11709
- var STRATEGIST_PROMPT_PATH = join12(__dirname4, "prompts", "strategist-system.md");
11765
+ import { join as join11, dirname as dirname5 } from "path";
11766
+ import { fileURLToPath as fileURLToPath3 } from "url";
11767
+ var __dirname3 = dirname5(fileURLToPath3(import.meta.url));
11768
+ var STRATEGIST_PROMPT_PATH = join11(__dirname3, "prompts", "strategist-system.md");
11710
11769
  var Strategist = class {
11711
11770
  llm;
11712
11771
  state;
@@ -11765,7 +11824,7 @@ var Strategist = class {
11765
11824
  }
11766
11825
  try {
11767
11826
  let journalSummary = "";
11768
- const summaryPath = join12(WORKSPACE.TURNS, "summary.md");
11827
+ const summaryPath = join11(WORKSPACE.TURNS, "summary.md");
11769
11828
  if (existsSync10(summaryPath)) {
11770
11829
  journalSummary = readFileSync7(summaryPath, "utf-8").trim();
11771
11830
  }
@@ -11888,6 +11947,198 @@ Detect stalls (repeated failures, no progress) and force completely different at
11888
11947
  Chain every finding: "If X works \u2192 immediately do Y \u2192 which enables Z."
11889
11948
  Maximum 50 lines. Zero preamble. Direct imperatives only. Never repeat failed approaches.`;
11890
11949
 
11950
+ // src/agents/user-input-queue.ts
11951
+ var RECENT_MESSAGE_THRESHOLD_SECONDS = 5;
11952
+ var USER_INPUT_INTENT_PROMPT = `
11953
+ <user-message priority="INTERRUPT">
11954
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
11955
+ \u26A1 USER MESSAGE \u2014 STOP. ANALYZE INTENT. THEN ACT.
11956
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
11957
+
11958
+ The user sent the following message(s) WHILE you are actively working.
11959
+ This message takes PRECEDENCE over your current plan. Process it NOW.
11960
+
11961
+ <<USER_MESSAGES>>
11962
+
11963
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
11964
+ \xA71. INTENT CLASSIFICATION (Chain-of-Thought \u2014 MANDATORY)
11965
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
11966
+
11967
+ You MUST internally reason through this decision tree IN ORDER.
11968
+ Stop at the FIRST matching category \u2014 do NOT skip ahead.
11969
+
11970
+ \u250C\u2500 STEP 1: Is the user telling you to STOP or ABORT?
11971
+ \u2502 Signals: "stop", "abort", "cancel", "enough", "halt", "wait"
11972
+ \u2502 \u2192 CATEGORY: ABORT
11973
+ \u2502 \u2192 ACTION: Immediately stop current tool execution.
11974
+ \u2502 Use \`ask_user\` to confirm: "Understood, stopping. What would you like me to do next?"
11975
+ \u2502
11976
+ \u251C\u2500 STEP 2: Is the user CORRECTING you or saying you're WRONG?
11977
+ \u2502 Signals: "that's wrong", "don't do that", "stop doing X", "you already tried that",
11978
+ \u2502 "not that way", "I said X not Y", negative feedback on your actions
11979
+ \u2502 \u2192 CATEGORY: CORRECTION
11980
+ \u2502 \u2192 ACTION: 1. Acknowledge the error briefly.
11981
+ \u2502 2. State what you will do differently.
11982
+ \u2502 3. Resume work with the corrected approach.
11983
+ \u2502 Do NOT use \`ask_user\` unless clarification is needed.
11984
+ \u2502
11985
+ \u251C\u2500 STEP 3: Is the user providing ACTIONABLE INFORMATION?
11986
+ \u2502 Signals: credentials, passwords, usernames, file paths, endpoints, API keys,
11987
+ \u2502 ports, version numbers, IP addresses, hints about the target
11988
+ \u2502 \u2192 CATEGORY: INFORMATION
11989
+ \u2502 \u2192 ACTION: 1. Store with \`add_loot\` (if credentials) or remember contextually.
11990
+ \u2502 2. USE this information immediately in your next tool call.
11991
+ \u2502 3. Do NOT ask "should I use this?" \u2014 just use it.
11992
+ \u2502 Example: User says "password is admin123"
11993
+ \u2502 \u2192 Immediately try those credentials on all discovered login surfaces.
11994
+ \u2502
11995
+ \u251C\u2500 STEP 4: Is the user giving a DIRECT COMMAND to execute something?
11996
+ \u2502 Signals: imperative verb + specific action: "run X", "scan Y", "exploit Z",
11997
+ \u2502 "try X on Y", "use sqlmap", "brute force SSH"
11998
+ \u2502 \u2192 CATEGORY: COMMAND
11999
+ \u2502 \u2192 ACTION: Execute EXACTLY what the user asked. No more, no less.
12000
+ \u2502 Do NOT add extra scans or "while we're at it" actions.
12001
+ \u2502 Do NOT ask for confirmation \u2014 the user already decided.
12002
+ \u2502
12003
+ \u251C\u2500 STEP 5: Is the user changing the TARGET or SCOPE?
12004
+ \u2502 Signals: new IP/domain, "switch to", "add target", "remove target",
12005
+ \u2502 "also attack X", "change scope"
12006
+ \u2502 \u2192 CATEGORY: TARGET_CHANGE
12007
+ \u2502 \u2192 ACTION: 1. Call \`add_target\` with the new target.
12008
+ \u2502 2. Confirm briefly with \`ask_user\`: "Added [target]. Starting reconnaissance."
12009
+ \u2502 3. Begin testing the new target.
12010
+ \u2502
12011
+ \u251C\u2500 STEP 6: Is the user providing STRATEGIC GUIDANCE?
12012
+ \u2502 Signals: "focus on X", "prioritize Y", "skip Z", "try X approach",
12013
+ \u2502 "what about X?", "have you considered X?", tactical suggestions
12014
+ \u2502 \u2192 CATEGORY: GUIDANCE
12015
+ \u2502 \u2192 ACTION: 1. Acknowledge the guidance briefly via \`ask_user\`:
12016
+ \u2502 "Understood \u2014 adjusting strategy to focus on [X]."
12017
+ \u2502 2. Immediately adjust your approach and continue working.
12018
+ \u2502 3. The acknowledgment and next action should be in the SAME turn.
12019
+ \u2502
12020
+ \u251C\u2500 STEP 7: Is the user asking about PROGRESS or STATUS?
12021
+ \u2502 Signals: "what did you find?", "any progress?", "status?", "what are you doing?",
12022
+ \u2502 "show me", "report", "findings so far", "how's it going?"
12023
+ \u2502 \u2192 CATEGORY: STATUS_QUERY
12024
+ \u2502 \u2192 ACTION: Use \`ask_user\` to provide a structured status report:
12025
+ \u2502 FORMAT:
12026
+ \u2502 "\u{1F4CA} Status Report:
12027
+ \u2502 \u2022 Phase: [current phase]
12028
+ \u2502 \u2022 Targets: [count] ([list key ones])
12029
+ \u2502 \u2022 Key Findings: [count] ([summarize top findings])
12030
+ \u2502 \u2022 Current Action: [what you were doing]
12031
+ \u2502 \u2022 Next Steps: [what you plan to do next]"
12032
+ \u2502 Then RESUME your previous work \u2014 do NOT stop after reporting.
12033
+ \u2502
12034
+ \u2514\u2500 STEP 8: Everything else \u2192 CONVERSATION
12035
+ Signals: greetings, questions, discussions, explanations, opinions,
12036
+ casual talk, "hello", "how does X work?", "explain Y"
12037
+ \u2192 CATEGORY: CONVERSATION
12038
+ \u2192 ACTION: Use \`ask_user\` to respond naturally and conversationally.
12039
+ Answer questions with your knowledge.
12040
+ Then ask if they want you to continue with the current task.
12041
+ Do NOT start any scans or attacks.
12042
+
12043
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12044
+ \xA72. MULTI-MESSAGE RESOLUTION
12045
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12046
+
12047
+ If multiple user messages are queued, process them as follows:
12048
+ 1. Read ALL messages first to understand the full context.
12049
+ 2. Later messages may OVERRIDE or CLARIFY earlier ones.
12050
+ Example: [1] "try brute force" \u2192 [2] "actually, skip brute force, try SQLi"
12051
+ \u2192 Only execute the SQLi instruction (message 2 overrides message 1).
12052
+ 3. If messages are independent, process the HIGHEST PRIORITY category first
12053
+ (ABORT > CORRECTION > INFORMATION > COMMAND > TARGET > GUIDANCE > STATUS > CONVERSATION).
12054
+ 4. Acknowledge all messages but act on the most recent directive.
12055
+
12056
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12057
+ \xA73. WORK RESUMPTION RULES
12058
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12059
+
12060
+ After handling the user's message, you MUST resume productive work:
12061
+
12062
+ \u251C\u2500 ABORT/CONVERSATION \u2192 Wait for next user instruction. Do NOT auto-resume.
12063
+ \u251C\u2500 STATUS_QUERY \u2192 Report status, then RESUME previous work in the same turn.
12064
+ \u251C\u2500 GUIDANCE/CORRECTION \u2192 Adjust plan, then CONTINUE with modified approach.
12065
+ \u251C\u2500 INFORMATION \u2192 USE the information immediately in your next action.
12066
+ \u251C\u2500 COMMAND \u2192 Execute the command. After completion, resume prior work.
12067
+ \u251C\u2500 TARGET_CHANGE \u2192 Switch to new target, begin fresh workflow.
12068
+
12069
+ KEY PRINCIPLE: Never leave a turn empty-handed.
12070
+ If you used \`ask_user\` to respond, you may ALSO call other tools in the same turn
12071
+ (except for ABORT and CONVERSATION categories).
12072
+
12073
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12074
+ \xA74. ANTI-PATTERNS \u2014 NEVER DO THESE
12075
+ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12076
+
12077
+ \u251C\u2500 \u274C Ignore the user's message and continue your previous plan
12078
+ \u251C\u2500 \u274C Start scanning/attacking after a greeting
12079
+ \u251C\u2500 \u274C Ask "should I use this?" when the user provides credentials \u2192 JUST USE THEM
12080
+ \u251C\u2500 \u274C Respond with only text (no tool call) \u2014 always use \`ask_user\` for responses
12081
+ \u251C\u2500 \u274C Stop all work after answering a status query \u2014 RESUME immediately
12082
+ \u251C\u2500 \u274C Add extra actions the user didn't ask for when handling a COMMAND
12083
+ \u251C\u2500 \u274C Repeat the same failed approach after a CORRECTION
12084
+ \u2514\u2500 \u274C Treat every input as an attack command \u2014 MOST inputs are collaborative
12085
+ </user-message>`;
12086
+ var UserInputQueue = class {
12087
+ queue = [];
12088
+ /**
12089
+ * Add a user input to the queue.
12090
+ * Called from TUI when user submits text during active agent processing.
12091
+ */
12092
+ enqueue(text) {
12093
+ this.queue.push({
12094
+ text,
12095
+ timestamp: Date.now()
12096
+ });
12097
+ }
12098
+ /**
12099
+ * Check if there are pending user inputs.
12100
+ */
12101
+ hasPending() {
12102
+ return this.queue.length > 0;
12103
+ }
12104
+ /**
12105
+ * Get the count of pending inputs.
12106
+ */
12107
+ pendingCount() {
12108
+ return this.queue.length;
12109
+ }
12110
+ /**
12111
+ * Drain all queued inputs and format them with intent analysis prompt.
12112
+ * Returns null if queue is empty.
12113
+ * Clears the queue after draining.
12114
+ */
12115
+ drainAndFormat() {
12116
+ if (this.queue.length === 0) return null;
12117
+ const messages = [...this.queue];
12118
+ this.queue = [];
12119
+ const formattedMessages = messages.map((m, i) => {
12120
+ const timeAgo = Math.round((Date.now() - m.timestamp) / 1e3);
12121
+ const timeLabel = timeAgo < RECENT_MESSAGE_THRESHOLD_SECONDS ? "just now" : `${timeAgo}s ago`;
12122
+ return `[${i + 1}] (${timeLabel}) "${m.text}"`;
12123
+ }).join("\n");
12124
+ return USER_INPUT_INTENT_PROMPT.replace("<<USER_MESSAGES>>", formattedMessages);
12125
+ }
12126
+ /**
12127
+ * Peek at the queue without draining.
12128
+ * Useful for diagnostics or TUI display.
12129
+ */
12130
+ peek() {
12131
+ return this.queue;
12132
+ }
12133
+ /**
12134
+ * Clear the queue without processing.
12135
+ * Used during /clear or abort.
12136
+ */
12137
+ clear() {
12138
+ this.queue = [];
12139
+ }
12140
+ };
12141
+
11891
12142
  // src/shared/utils/turn-record.ts
11892
12143
  function formatTurnRecord(input) {
11893
12144
  const { turn, timestamp, phase, tools, memo: memo6, reflection } = input;
@@ -11975,7 +12226,7 @@ function formatReflectionInput(input) {
11975
12226
 
11976
12227
  // src/agents/main-agent.ts
11977
12228
  import { writeFileSync as writeFileSync9, existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
11978
- import { join as join13 } from "path";
12229
+ import { join as join12 } from "path";
11979
12230
  var MainAgent = class extends CoreAgent {
11980
12231
  promptBuilder;
11981
12232
  strategist;
@@ -11984,6 +12235,11 @@ var MainAgent = class extends CoreAgent {
11984
12235
  userInput = "";
11985
12236
  /** Monotonic turn counter for journal entries */
11986
12237
  turnCounter = 0;
12238
+ /**
12239
+ * Queue for user inputs received during agent execution.
12240
+ * Inputs are drained and injected at iteration boundaries.
12241
+ */
12242
+ userInputQueue = new UserInputQueue();
11987
12243
  constructor(state, events, toolRegistry, approvalGate, scopeGuard) {
11988
12244
  super(AGENT_ROLES.ORCHESTRATOR, state, events, toolRegistry);
11989
12245
  this.approvalGate = approvalGate;
@@ -12024,6 +12280,15 @@ var MainAgent = class extends CoreAgent {
12024
12280
  if (this.turnCounter === 0) {
12025
12281
  this.turnCounter = getNextTurnNumber();
12026
12282
  }
12283
+ if (this.userInputQueue.hasPending()) {
12284
+ const userMessage = this.userInputQueue.drainAndFormat();
12285
+ if (userMessage) {
12286
+ messages.push({
12287
+ role: "user",
12288
+ content: userMessage
12289
+ });
12290
+ }
12291
+ }
12027
12292
  this.turnToolJournal = [];
12028
12293
  this.turnMemo = { keyFindings: [], credentials: [], attackVectors: [], failures: [], suspicions: [], attackValue: "LOW", nextSteps: [] };
12029
12294
  this.turnReflections = [];
@@ -12083,7 +12348,7 @@ ${extraction.content.trim()}
12083
12348
  ensureDirExists(WORKSPACE.TURNS);
12084
12349
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
12085
12350
  const turnFileName = `turn-${String(this.turnCounter).padStart(4, "0")}_${ts}.md`;
12086
- const turnPath = join13(WORKSPACE.TURNS, turnFileName);
12351
+ const turnPath = join12(WORKSPACE.TURNS, turnFileName);
12087
12352
  const turnContent = formatTurnRecord({
12088
12353
  turn: this.turnCounter,
12089
12354
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -12096,7 +12361,7 @@ ${extraction.content.trim()}
12096
12361
  } catch {
12097
12362
  }
12098
12363
  try {
12099
- const summaryPath = join13(WORKSPACE.TURNS, "summary.md");
12364
+ const summaryPath = join12(WORKSPACE.TURNS, "summary.md");
12100
12365
  const existingSummary = existsSync11(summaryPath) ? readFileSync8(summaryPath, "utf-8") : "";
12101
12366
  const turnData = formatTurnRecord({
12102
12367
  turn: this.turnCounter,
@@ -12213,6 +12478,7 @@ ${turnData}`
12213
12478
  });
12214
12479
  this.state.reset();
12215
12480
  this.userInput = "";
12481
+ this.userInputQueue.clear();
12216
12482
  this.strategist.reset();
12217
12483
  return clearWorkspace();
12218
12484
  }
@@ -12239,6 +12505,20 @@ ${turnData}`
12239
12505
  getStrategist() {
12240
12506
  return this.strategist;
12241
12507
  }
12508
+ /**
12509
+ * Enqueue a user input for processing at the next iteration boundary.
12510
+ * Called from TUI when user sends a message while agent is actively processing.
12511
+ */
12512
+ enqueueUserInput(text) {
12513
+ this.userInputQueue.enqueue(text);
12514
+ }
12515
+ /**
12516
+ * Check if there are pending user inputs in the queue.
12517
+ * Useful for TUI to show pending input indicator.
12518
+ */
12519
+ hasPendingUserInput() {
12520
+ return this.userInputQueue.hasPending();
12521
+ }
12242
12522
  };
12243
12523
 
12244
12524
  // src/agents/factory.ts
@@ -12719,7 +12999,7 @@ ${firstLine}`);
12719
12999
  setInputHandler((p) => {
12720
13000
  return new Promise((resolve) => {
12721
13001
  const isPassword = /password|passphrase/i.test(p);
12722
- const inputType = /sudo/i.test(p) ? "sudo_password" : isPassword ? "password" : "text";
13002
+ const inputType = /sudo/i.test(p) ? INPUT_TYPES.SUDO_PASSWORD : isPassword ? INPUT_TYPES.PASSWORD : INPUT_TYPES.TEXT;
12723
13003
  setInputRequest({
12724
13004
  status: "active",
12725
13005
  prompt: p.trim(),
@@ -12731,8 +13011,7 @@ ${firstLine}`);
12731
13011
  });
12732
13012
  setCredentialHandler((request) => {
12733
13013
  return new Promise((resolve) => {
12734
- const hiddenTypes = ["password", "sudo_password", "ssh_password", "passphrase", "api_key", "credential"];
12735
- const isPassword = hiddenTypes.includes(request.type);
13014
+ const isPassword = SENSITIVE_INPUT_TYPES.includes(request.type);
12736
13015
  const displayPrompt = buildCredentialPrompt(request);
12737
13016
  setInputRequest({
12738
13017
  status: "active",
@@ -13583,62 +13862,60 @@ var App = ({ autoApprove = false, target }) => {
13583
13862
  executeTask(args.join(" ") || `Perform comprehensive penetration testing${targetInfo}`);
13584
13863
  break;
13585
13864
  case UI_COMMANDS.FINDINGS:
13586
- case UI_COMMANDS.FINDINGS_SHORT:
13865
+ case UI_COMMANDS.FINDINGS_SHORT: {
13587
13866
  const findings = agent.getState().getFindings();
13588
13867
  if (!findings.length) {
13589
13868
  addMessage("system", "No findings.");
13590
13869
  break;
13591
13870
  }
13592
- const severityOrder = ["critical", "high", "medium", "low", "info"];
13593
- const severityIcons = {
13594
- critical: "\u{1F534}",
13595
- high: "\u{1F7E0}",
13596
- medium: "\u{1F7E1}",
13597
- low: "\u{1F7E2}",
13598
- info: "\u26AA"
13871
+ const sorted = [...findings].sort((a, b) => b.confidence - a.confidence);
13872
+ const confIcon = (c) => {
13873
+ if (c >= 100) return "\u{1F534}";
13874
+ if (c >= CONFIDENCE_THRESHOLDS.CONFIRMED) return "\u{1F7E0}";
13875
+ if (c >= CONFIDENCE_THRESHOLDS.PROBABLE) return "\u{1F7E1}";
13876
+ if (c >= CONFIDENCE_THRESHOLDS.POSSIBLE) return "\u{1F7E2}";
13877
+ return "\u26AA";
13878
+ };
13879
+ const confLabel = (c) => {
13880
+ if (c >= CONFIDENCE_THRESHOLDS.CONFIRMED) return "confirmed";
13881
+ if (c >= CONFIDENCE_THRESHOLDS.PROBABLE) return "probable";
13882
+ if (c >= CONFIDENCE_THRESHOLDS.POSSIBLE) return "possible";
13883
+ return "speculative";
13599
13884
  };
13600
- const grouped = {};
13601
- for (const f of findings) {
13602
- const sev = f.severity.toLowerCase();
13603
- if (!grouped[sev]) grouped[sev] = [];
13604
- grouped[sev].push(f);
13605
- }
13606
13885
  const findingLines = [];
13607
- const sevCounts = severityOrder.filter((s) => grouped[s]?.length).map((s) => `${severityIcons[s]} ${s.toUpperCase()}: ${grouped[s].length}`).join(" ");
13608
- findingLines.push(`\u2500\u2500\u2500 ${findings.length} Findings \u2500\u2500 ${sevCounts} \u2500\u2500\u2500`);
13886
+ const nConfirmed = sorted.filter((f) => f.confidence >= CONFIDENCE_THRESHOLDS.CONFIRMED).length;
13887
+ const nProbable = sorted.filter((f) => f.confidence >= CONFIDENCE_THRESHOLDS.PROBABLE && f.confidence < CONFIDENCE_THRESHOLDS.CONFIRMED).length;
13888
+ const nPossible = sorted.filter((f) => f.confidence < CONFIDENCE_THRESHOLDS.PROBABLE).length;
13889
+ findingLines.push(`\u2500\u2500\u2500 ${findings.length} Findings \u2500\u2500 \u{1F534}\u{1F7E0} confirmed:${nConfirmed} \u{1F7E1} probable:${nProbable} \u{1F7E2}\u26AA possible:${nPossible} \u2500\u2500\u2500`);
13609
13890
  findingLines.push("");
13610
- for (const sev of severityOrder) {
13611
- const group = grouped[sev];
13612
- if (!group?.length) continue;
13613
- const icon = severityIcons[sev] || "\u2022";
13614
- findingLines.push(`${icon} \u2500\u2500 ${sev.toUpperCase()} (${group.length}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
13891
+ sorted.forEach((f, i) => {
13892
+ const icon = confIcon(f.confidence);
13893
+ const label = confLabel(f.confidence);
13894
+ const scoreBar = `[${String(f.confidence).padStart(3, " ")}/100]`;
13895
+ const atk = f.attackPattern ? ` \u2502 ATT&CK: ${f.attackPattern}` : "";
13896
+ const cat = f.category ? ` \u2502 ${f.category}` : "";
13897
+ findingLines.push(` ${icon} ${scoreBar} ${f.title}`);
13898
+ findingLines.push(` ${label.toUpperCase()} \u2502 ${f.severity.toUpperCase()}${atk}${cat}`);
13899
+ if (f.affected.length > 0) {
13900
+ findingLines.push(` Affected: ${f.affected.join(", ")}`);
13901
+ }
13902
+ if (f.description) {
13903
+ findingLines.push(` ${f.description}`);
13904
+ }
13905
+ if (f.evidence.length > 0) {
13906
+ findingLines.push(` Evidence:`);
13907
+ f.evidence.forEach((e) => {
13908
+ findingLines.push(` \u25B8 ${e}`);
13909
+ });
13910
+ }
13911
+ if (f.remediation) {
13912
+ findingLines.push(` Fix: ${f.remediation}`);
13913
+ }
13615
13914
  findingLines.push("");
13616
- group.forEach((f, i) => {
13617
- const verified = f.isVerified ? `\u2713 Verified` : `? Unverified`;
13618
- const atk = f.attackPattern ? ` \u2502 ATT&CK: ${f.attackPattern}` : "";
13619
- const cat = f.category ? ` \u2502 ${f.category}` : "";
13620
- findingLines.push(` [${i + 1}] ${f.title}`);
13621
- findingLines.push(` ${verified}${atk}${cat}`);
13622
- if (f.affected.length > 0) {
13623
- findingLines.push(` Affected: ${f.affected.join(", ")}`);
13624
- }
13625
- if (f.description) {
13626
- findingLines.push(` ${f.description}`);
13627
- }
13628
- if (f.evidence.length > 0) {
13629
- findingLines.push(` Evidence:`);
13630
- f.evidence.forEach((e) => {
13631
- findingLines.push(` \u25B8 ${e}`);
13632
- });
13633
- }
13634
- if (f.remediation) {
13635
- findingLines.push(` Fix: ${f.remediation}`);
13636
- }
13637
- findingLines.push("");
13638
- });
13639
- }
13915
+ });
13640
13916
  addMessage("system", findingLines.join("\n"));
13641
13917
  break;
13918
+ }
13642
13919
  case UI_COMMANDS.ASSETS:
13643
13920
  case UI_COMMANDS.ASSETS_SHORT:
13644
13921
  addMessage("status", formatInlineStatus());
@@ -13695,10 +13972,13 @@ ${procData.stdout || "(no output)"}
13695
13972
  if (trimmed.startsWith("/")) {
13696
13973
  const [cmd, ...args] = trimmed.slice(1).split(" ");
13697
13974
  await handleCommand(cmd, args);
13975
+ } else if (isProcessingRef.current) {
13976
+ agent.enqueueUserInput(trimmed);
13977
+ addMessage("system", "\u{1F4AC} Message queued \u2014 will be processed at next iteration");
13698
13978
  } else {
13699
13979
  await executeTask(trimmed);
13700
13980
  }
13701
- }, [addMessage, executeTask, handleCommand]);
13981
+ }, [agent, addMessage, executeTask, handleCommand]);
13702
13982
  const handleSecretSubmit = useCallback4((value) => {
13703
13983
  const ir = inputRequestRef.current;
13704
13984
  if (ir.status !== "active") return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pentesting",
3
- "version": "0.49.2",
3
+ "version": "0.49.3",
4
4
  "description": "Autonomous Penetration Testing AI Agent",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",