@wrongstack/core 0.5.3 → 0.5.5

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 (47) hide show
  1. package/dist/{agent-bridge-EiUFe3if.d.ts → agent-bridge-B07AYFBk.d.ts} +1 -1
  2. package/dist/{compactor-BP4xhKVi.d.ts → compactor-BWhJXxsW.d.ts} +1 -1
  3. package/dist/{config-BOD_HQBw.d.ts → config-BgM0BIpz.d.ts} +1 -1
  4. package/dist/{context-PH4DvBZV.d.ts → context-CLZXPPYk.d.ts} +18 -1
  5. package/dist/coordination/index.d.ts +9 -9
  6. package/dist/defaults/index.d.ts +19 -19
  7. package/dist/defaults/index.js +50 -2
  8. package/dist/defaults/index.js.map +1 -1
  9. package/dist/{director-state-CVzkjKRZ.d.ts → director-state-BUxlqkOa.d.ts} +5 -5
  10. package/dist/{events-oxn-Wkub.d.ts → events-qnDZbrtb.d.ts} +67 -2
  11. package/dist/execution/index.d.ts +12 -12
  12. package/dist/extension/index.d.ts +6 -6
  13. package/dist/{index-a12jc7-r.d.ts → index-DPLJw_ZI.d.ts} +5 -5
  14. package/dist/{index-CcbWbcpy.d.ts → index-De4R4rQ7.d.ts} +5 -5
  15. package/dist/index.d.ts +328 -27
  16. package/dist/index.js +1996 -14
  17. package/dist/index.js.map +1 -1
  18. package/dist/infrastructure/index.d.ts +6 -6
  19. package/dist/kernel/index.d.ts +9 -9
  20. package/dist/kernel/index.js +98 -1
  21. package/dist/kernel/index.js.map +1 -1
  22. package/dist/{mcp-servers-uPmBxZ1B.d.ts → mcp-servers-CSMfaBuL.d.ts} +3 -3
  23. package/dist/models/index.d.ts +2 -2
  24. package/dist/{multi-agent-D6S4Z7H8.d.ts → multi-agent-Cv8wk47i.d.ts} +2 -2
  25. package/dist/observability/index.d.ts +2 -2
  26. package/dist/{path-resolver-CprD5DhS.d.ts → path-resolver-DiCUvEg6.d.ts} +2 -2
  27. package/dist/{provider-runner-DGisz_lG.d.ts → provider-runner-3SHqk9zB.d.ts} +3 -3
  28. package/dist/{retry-policy-DUJ8-Qc_.d.ts → retry-policy-LLUxJmYY.d.ts} +1 -1
  29. package/dist/sdd/index.d.ts +3 -3
  30. package/dist/{secret-scrubber-CB11A2P7.d.ts → secret-scrubber-BhJTNr9v.d.ts} +4 -2
  31. package/dist/{secret-scrubber-EqFa0SyI.d.ts → secret-scrubber-Z_VXuXQT.d.ts} +1 -1
  32. package/dist/security/index.d.ts +13 -3
  33. package/dist/security/index.js +34 -1
  34. package/dist/security/index.js.map +1 -1
  35. package/dist/{selector-yqyxt-Ii.d.ts → selector-DB2-byKH.d.ts} +1 -1
  36. package/dist/{session-reader-1tOyoY1s.d.ts → session-reader-4jxsYLZ8.d.ts} +1 -1
  37. package/dist/storage/index.d.ts +6 -6
  38. package/dist/storage/index.js +16 -1
  39. package/dist/storage/index.js.map +1 -1
  40. package/dist/{system-prompt-BJlzKGO6.d.ts → system-prompt-DI4Dtc1I.d.ts} +1 -1
  41. package/dist/{tool-executor-B6kRcWeF.d.ts → tool-executor-DSvmOBe6.d.ts} +4 -4
  42. package/dist/types/index.d.ts +15 -15
  43. package/dist/types/index.js +9 -1
  44. package/dist/types/index.js.map +1 -1
  45. package/dist/utils/index.d.ts +2 -2
  46. package/dist/{wstack-paths-BGu2INTm.d.ts → wstack-paths-86YPFktR.d.ts} +1 -1
  47. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,9 @@ import * as crypto2 from 'crypto';
2
2
  import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash } from 'crypto';
3
3
  import * as fs from 'fs';
4
4
  import * as fsp2 from 'fs/promises';
5
+ import { readFile, readdir, writeFile, stat, mkdir } from 'fs/promises';
5
6
  import * as path6 from 'path';
7
+ import { join, extname, relative } from 'path';
6
8
  import * as os4 from 'os';
7
9
  import { EventEmitter } from 'events';
8
10
  import { createGunzip } from 'zlib';
@@ -506,6 +508,103 @@ var EventBus = class {
506
508
  return this.wildcards.length;
507
509
  }
508
510
  };
511
+ var ScopedEventBus = class extends EventBus {
512
+ // Track registrations by a unique counter key so that EventBus.once()'s
513
+ // internal listener-removal doesn't affect our tracking (once removes the
514
+ // fn from EventBus but we still need to call our unsub during teardown).
515
+ registrations = /* @__PURE__ */ new Map();
516
+ nextKey = 0;
517
+ /**
518
+ * Identical to `EventBus.on` but the listener is tracked so that
519
+ * `teardown()` will remove it automatically.
520
+ */
521
+ on(event, fn) {
522
+ const key = this.nextKey++;
523
+ const unsub = super.on(event, fn);
524
+ this.registrations.set(key, unsub);
525
+ return () => {
526
+ this.registrations.delete(key);
527
+ unsub();
528
+ };
529
+ }
530
+ /**
531
+ * Identical to `EventBus.once` but the listener is tracked so that
532
+ * `teardown()` will remove it automatically.
533
+ *
534
+ * Uses EventBus's public API directly to avoid triggering our own `on()`
535
+ * override (which would consume a key slot for the wrapper, then orphan
536
+ * our registration entry under a different key).
537
+ */
538
+ once(event, fn) {
539
+ const key = this.nextKey++;
540
+ const wrapper = (payload) => {
541
+ EventBus.prototype.off.call(this, event, wrapper);
542
+ fn(payload);
543
+ };
544
+ EventBus.prototype.on.call(this, event, wrapper);
545
+ const unsub = () => {
546
+ this.registrations.delete(key);
547
+ EventBus.prototype.off.call(this, event, wrapper);
548
+ };
549
+ this.registrations.set(key, unsub);
550
+ return unsub;
551
+ }
552
+ /**
553
+ * Identical to `EventBus.onPattern` but the listener is tracked so that
554
+ * `teardown()` will remove it automatically.
555
+ */
556
+ onPattern(pattern, fn) {
557
+ const key = this.nextKey++;
558
+ const unsub = super.onPattern(pattern, fn);
559
+ this.registrations.set(key, unsub);
560
+ return () => {
561
+ this.registrations.delete(key);
562
+ unsub();
563
+ };
564
+ }
565
+ /**
566
+ * Identical to `EventBus.onRegex` but the listener is tracked so that
567
+ * `teardown()` will remove it automatically.
568
+ */
569
+ onRegex(regex, fn) {
570
+ const key = this.nextKey++;
571
+ const unsub = super.onRegex(regex, fn);
572
+ this.registrations.set(key, unsub);
573
+ return () => {
574
+ this.registrations.delete(key);
575
+ unsub();
576
+ };
577
+ }
578
+ /**
579
+ * Remove every listener that was registered through this scoped bus.
580
+ * Idempotent — calling it multiple times is safe.
581
+ *
582
+ * Also available as `[Symbol.dispose]` for explicit resource management:
583
+ * ```ts
584
+ * using scope = new ScopedEventBus();
585
+ * scope.on('tool.executed', handler);
586
+ * // automatically teardown()'d when scope exits
587
+ * ```
588
+ */
589
+ teardown() {
590
+ for (const unsub of this.registrations.values()) {
591
+ try {
592
+ unsub();
593
+ } catch {
594
+ }
595
+ }
596
+ this.registrations.clear();
597
+ this.clear();
598
+ }
599
+ /** Alias for `teardown()` — enables `using new ScopedEventBus()` in Node ≥ 22. */
600
+ [Symbol.dispose]() {
601
+ this.teardown();
602
+ }
603
+ /** Number of tracked registrations. */
604
+ get scopedListenerCount() {
605
+ return this.registrations.size;
606
+ }
607
+ };
509
608
  function makePatternMatcher(pattern) {
510
609
  if (pattern === "*") return () => true;
511
610
  if (pattern.endsWith(".*")) {
@@ -1688,7 +1787,15 @@ var PATTERNS = [
1688
1787
  type: "stripe_key",
1689
1788
  regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g
1690
1789
  },
1691
- { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
1790
+ {
1791
+ type: "twilio_sid",
1792
+ regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g
1793
+ },
1794
+ {
1795
+ type: "telegram_bot_token",
1796
+ // Telegram tokens are of the form bot<digits>:<alphanum> in URL paths
1797
+ regex: /\/bot\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
1798
+ },
1692
1799
  {
1693
1800
  type: "jwt",
1694
1801
  // Anchored: look for literal "eyJ" which is unambiguous for JWT header
@@ -1786,8 +1893,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
1786
1893
  }
1787
1894
  let mode;
1788
1895
  try {
1789
- const stat6 = await fsp2.stat(targetPath);
1790
- mode = stat6.mode & 511;
1896
+ const stat8 = await fsp2.stat(targetPath);
1897
+ mode = stat8.mode & 511;
1791
1898
  } catch {
1792
1899
  mode = opts.mode;
1793
1900
  }
@@ -3827,8 +3934,8 @@ var DefaultSessionStore = class {
3827
3934
  return JSON.parse(raw);
3828
3935
  } catch {
3829
3936
  const full = path6.join(this.dir, `${id}.jsonl`);
3830
- const stat6 = await fsp2.stat(full);
3831
- const summary = await this.summarize(id, stat6.mtime.toISOString());
3937
+ const stat8 = await fsp2.stat(full);
3938
+ const summary = await this.summarize(id, stat8.mtime.toISOString());
3832
3939
  await fsp2.writeFile(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
3833
3940
  console.warn(
3834
3941
  `[session-store] Failed to write manifest for "${id}":`,
@@ -4495,6 +4602,16 @@ function labelOf(scope) {
4495
4602
  }
4496
4603
 
4497
4604
  // src/storage/config-store.ts
4605
+ function stripEphemeralFields(cfg) {
4606
+ const env = cfg._envSource;
4607
+ if (!env?.size) return cfg;
4608
+ const out = { ...cfg };
4609
+ for (const field of env) {
4610
+ delete out[field];
4611
+ }
4612
+ delete out._envSource;
4613
+ return out;
4614
+ }
4498
4615
  var DefaultConfigStore = class {
4499
4616
  current;
4500
4617
  watchers = /* @__PURE__ */ new Set();
@@ -4512,7 +4629,8 @@ var DefaultConfigStore = class {
4512
4629
  return ext ? ext : FROZEN_EMPTY;
4513
4630
  }
4514
4631
  update(partial) {
4515
- const next = deepFreeze(structuredClone({ ...this.current, ...partial }));
4632
+ const scrubbed = stripEphemeralFields(partial);
4633
+ const next = deepFreeze(structuredClone({ ...this.current, ...scrubbed }));
4516
4634
  if (next.version !== 1) {
4517
4635
  throw new Error(`ConfigStore.update: version must remain 1, got ${String(next.version)}`);
4518
4636
  }
@@ -4617,15 +4735,19 @@ var BEHAVIOR_DEFAULTS = {
4617
4735
  var ENV_MAP = {
4618
4736
  WRONGSTACK_PROVIDER: (c, v) => {
4619
4737
  c.provider = v;
4738
+ (c._envSource ??= /* @__PURE__ */ new Set()).add("provider");
4620
4739
  },
4621
4740
  WRONGSTACK_MODEL: (c, v) => {
4622
4741
  c.model = v;
4742
+ (c._envSource ??= /* @__PURE__ */ new Set()).add("model");
4623
4743
  },
4624
4744
  WRONGSTACK_API_KEY: (c, v) => {
4625
4745
  c.apiKey = v;
4746
+ (c._envSource ??= /* @__PURE__ */ new Set()).add("apiKey");
4626
4747
  },
4627
4748
  WRONGSTACK_BASE_URL: (c, v) => {
4628
4749
  c.baseUrl = v;
4750
+ (c._envSource ??= /* @__PURE__ */ new Set()).add("baseUrl");
4629
4751
  },
4630
4752
  WRONGSTACK_LOG_LEVEL: (c, v) => {
4631
4753
  if (!c.log) c.log = { level: "info" };
@@ -5346,6 +5468,7 @@ var DefaultPermissionPolicy = class {
5346
5468
  loaded = false;
5347
5469
  trustFile;
5348
5470
  yolo;
5471
+ forceAllYolo;
5349
5472
  /**
5350
5473
  * Session-scoped "soft deny" map. When the user presses 'n' (block once),
5351
5474
  * the tool+pattern is added here. If the LLM retries in the same session,
@@ -5378,6 +5501,7 @@ var DefaultPermissionPolicy = class {
5378
5501
  constructor(opts) {
5379
5502
  this.trustFile = opts.trustFile;
5380
5503
  this.yolo = opts.yolo ?? false;
5504
+ this.forceAllYolo = opts.forceAllYolo ?? false;
5381
5505
  this.promptDelegate = opts.promptDelegate;
5382
5506
  }
5383
5507
  /**
@@ -5397,6 +5521,14 @@ var DefaultPermissionPolicy = class {
5397
5521
  getYolo() {
5398
5522
  return this.yolo;
5399
5523
  }
5524
+ /** Toggle force-all-YOLO at runtime. */
5525
+ setForceAllYolo(enabled) {
5526
+ this.forceAllYolo = enabled;
5527
+ }
5528
+ /** Check whether force-all-YOLO is active. */
5529
+ getForceAllYolo() {
5530
+ return this.forceAllYolo;
5531
+ }
5400
5532
  async reload() {
5401
5533
  try {
5402
5534
  const raw = await fsp2.readFile(this.trustFile, "utf8");
@@ -5438,6 +5570,21 @@ var DefaultPermissionPolicy = class {
5438
5570
  return { permission: "auto", source: "trust" };
5439
5571
  }
5440
5572
  if (this.yolo) {
5573
+ if (tool.riskTier === "destructive" && !this.forceAllYolo) {
5574
+ if (this.promptDelegate) {
5575
+ const decision = await this.promptDelegate(tool, input, subject ?? tool.name);
5576
+ if (decision === "always") {
5577
+ await this.trust({ tool: tool.name, pattern: subject ?? tool.name });
5578
+ return { permission: "auto", source: "user", reason: "destructive yolo always-allowed" };
5579
+ }
5580
+ if (decision === "deny") {
5581
+ await this.deny({ tool: tool.name, pattern: subject ?? tool.name });
5582
+ return { permission: "deny", source: "user", reason: "user denied destructive yolo" };
5583
+ }
5584
+ return { permission: decision === "yes" ? "auto" : "deny", source: "user" };
5585
+ }
5586
+ return { permission: "confirm", source: "yolo_destructive", riskTier: "destructive", reason: "destructive tool needs explicit approval even in yolo mode" };
5587
+ }
5441
5588
  return { permission: "auto", source: "yolo" };
5442
5589
  }
5443
5590
  if (tool.name === "write" && subject) {
@@ -9179,8 +9326,8 @@ async function loadProjectModes(modesDir) {
9179
9326
  for (const entry of entries) {
9180
9327
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
9181
9328
  const filePath = path6.join(modesDir, entry);
9182
- const stat6 = await fsp2.stat(filePath);
9183
- if (!stat6.isFile()) continue;
9329
+ const stat8 = await fsp2.stat(filePath);
9330
+ if (!stat8.isFile()) continue;
9184
9331
  const content = await fsp2.readFile(filePath, "utf8");
9185
9332
  const id = path6.basename(entry, path6.extname(entry));
9186
9333
  modes.push({
@@ -12762,10 +12909,10 @@ var SkillInstaller = class {
12762
12909
  if (!resolved.startsWith(path6.resolve(destDir))) {
12763
12910
  throw new Error(`Path traversal detected in skill file: ${file}`);
12764
12911
  }
12765
- const stat6 = await fsp2.stat(srcPath);
12766
- if (stat6.size > MAX_SKILL_FILE_SIZE) {
12912
+ const stat8 = await fsp2.stat(srcPath);
12913
+ if (stat8.size > MAX_SKILL_FILE_SIZE) {
12767
12914
  throw new Error(
12768
- `Skill file "${file}" is too large (${(stat6.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
12915
+ `Skill file "${file}" is too large (${(stat8.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
12769
12916
  );
12770
12917
  }
12771
12918
  await fsp2.mkdir(path6.dirname(destPath), { recursive: true });
@@ -13161,6 +13308,1841 @@ async function revertSnapshots(snapshots) {
13161
13308
  }
13162
13309
  return { revertedFiles, errors };
13163
13310
  }
13311
+ var MATCHERS = {
13312
+ pnpmWorkspace: (files) => files.includes("pnpm-workspace.yaml"),
13313
+ gradlew: (_files, dirs) => dirs.includes("gradlew"),
13314
+ mavenWrapper: (_files, dirs) => dirs.includes(".mvn"),
13315
+ dotnetSdk: (files) => files.some((f) => f.endsWith(".csproj") || f.endsWith(".fsproj")),
13316
+ yarnConfig: (files) => files.includes(".yarnrc") || files.includes("yarn.config.js"),
13317
+ notPoetryLock: (files) => !files.includes("poetry.lock")
13318
+ };
13319
+ var STACK_SIGNATURES = [
13320
+ // Node.js variants - checked in order, first match wins
13321
+ {
13322
+ stack: "nodejs",
13323
+ packageManager: "pnpm",
13324
+ manifestFiles: ["package.json"],
13325
+ lockFiles: ["pnpm-lock.yaml"],
13326
+ secondarySignatures: (f) => MATCHERS.pnpmWorkspace(f)
13327
+ },
13328
+ {
13329
+ stack: "nodejs",
13330
+ packageManager: "bun",
13331
+ manifestFiles: ["package.json"],
13332
+ lockFiles: ["bun.lockb"]
13333
+ },
13334
+ {
13335
+ stack: "nodejs",
13336
+ packageManager: "yarn",
13337
+ manifestFiles: ["package.json"],
13338
+ lockFiles: ["yarn.lock"],
13339
+ secondarySignatures: (f) => MATCHERS.yarnConfig(f)
13340
+ },
13341
+ {
13342
+ stack: "nodejs",
13343
+ packageManager: "npm",
13344
+ manifestFiles: ["package.json"],
13345
+ lockFiles: ["package-lock.json"]
13346
+ },
13347
+ // Python variants
13348
+ {
13349
+ stack: "python",
13350
+ packageManager: "poetry",
13351
+ manifestFiles: ["pyproject.toml"],
13352
+ lockFiles: ["poetry.lock"]
13353
+ },
13354
+ {
13355
+ stack: "python",
13356
+ packageManager: "pip",
13357
+ manifestFiles: ["requirements.txt", "setup.py"],
13358
+ lockFiles: ["requirements.txt"]
13359
+ },
13360
+ {
13361
+ stack: "python",
13362
+ packageManager: "pip",
13363
+ manifestFiles: ["pyproject.toml"],
13364
+ lockFiles: [],
13365
+ secondarySignatures: (f) => MATCHERS.notPoetryLock(f)
13366
+ },
13367
+ // Rust
13368
+ {
13369
+ stack: "rust",
13370
+ packageManager: "cargo",
13371
+ manifestFiles: ["Cargo.toml"],
13372
+ lockFiles: ["Cargo.lock"]
13373
+ },
13374
+ // Go
13375
+ {
13376
+ stack: "go",
13377
+ packageManager: "go",
13378
+ manifestFiles: ["go.mod"],
13379
+ lockFiles: ["go.sum"]
13380
+ },
13381
+ // Java variants
13382
+ {
13383
+ stack: "java",
13384
+ packageManager: "gradle",
13385
+ manifestFiles: ["build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"],
13386
+ lockFiles: [],
13387
+ secondarySignatures: (_f, d) => MATCHERS.gradlew(_f, d)
13388
+ },
13389
+ {
13390
+ stack: "java",
13391
+ packageManager: "maven",
13392
+ manifestFiles: ["pom.xml"],
13393
+ lockFiles: [],
13394
+ secondarySignatures: (_f, d) => MATCHERS.mavenWrapper(_f, d)
13395
+ },
13396
+ // .NET
13397
+ {
13398
+ stack: "dotnet",
13399
+ packageManager: "nuget",
13400
+ manifestFiles: ["Directory.Build.props", "Directory.Packages.props"],
13401
+ lockFiles: ["packages.lock.json"],
13402
+ secondarySignatures: (f) => MATCHERS.dotnetSdk(f)
13403
+ },
13404
+ {
13405
+ stack: "dotnet",
13406
+ packageManager: "nuget",
13407
+ manifestFiles: ["*.csproj", "*.fsproj", "*.xproj"],
13408
+ lockFiles: [],
13409
+ secondarySignatures: (f) => MATCHERS.dotnetSdk(f)
13410
+ },
13411
+ // PHP
13412
+ {
13413
+ stack: "php",
13414
+ packageManager: "composer",
13415
+ manifestFiles: ["composer.json"],
13416
+ lockFiles: ["composer.lock"]
13417
+ },
13418
+ // Ruby
13419
+ {
13420
+ stack: "ruby",
13421
+ packageManager: "bundler",
13422
+ manifestFiles: ["Gemfile"],
13423
+ lockFiles: ["Gemfile.lock"]
13424
+ },
13425
+ // C++
13426
+ {
13427
+ stack: "cpp",
13428
+ packageManager: "cmake",
13429
+ manifestFiles: ["CMakeLists.txt"],
13430
+ lockFiles: [],
13431
+ secondarySignatures: (f) => f.includes("CMakeCache.txt") || f.includes("CMakePresets.json")
13432
+ },
13433
+ // Swift
13434
+ {
13435
+ stack: "swift",
13436
+ packageManager: "swiftpm",
13437
+ manifestFiles: ["Package.swift"],
13438
+ lockFiles: []
13439
+ }
13440
+ ];
13441
+ var MONOREPO_INDICATORS = {
13442
+ pnpm: ["pnpm-workspace.yaml"],
13443
+ npm: ["lerna.json", "nx.json"],
13444
+ yarn: [],
13445
+ bun: [],
13446
+ cargo: [],
13447
+ go: ["go.work"],
13448
+ maven: [],
13449
+ gradle: [],
13450
+ nuget: ["Directory.Build.props"],
13451
+ pip: [],
13452
+ poetry: [],
13453
+ bundler: [],
13454
+ cmake: [],
13455
+ swiftpm: [],
13456
+ unknown: []
13457
+ };
13458
+ var TechStackDetector = class {
13459
+ cachedResults = /* @__PURE__ */ new Map();
13460
+ async detect(projectRoot) {
13461
+ const cached = this.cachedResults.get(projectRoot);
13462
+ if (cached) return cached;
13463
+ const result = {
13464
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
13465
+ projectRoot,
13466
+ detectedStacks: [],
13467
+ isMonorepo: false,
13468
+ workspaceConfigs: []
13469
+ };
13470
+ const entries = await readdir(projectRoot, { withFileTypes: true });
13471
+ const files = entries.filter((e) => e.isFile()).map((e) => e.name);
13472
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
13473
+ const detectedStacks = /* @__PURE__ */ new Set();
13474
+ for (const signature of STACK_SIGNATURES) {
13475
+ const detected = this.matchSignature(signature, files, dirs);
13476
+ if (detected) {
13477
+ if (detectedStacks.has(signature.stack)) continue;
13478
+ detectedStacks.add(signature.stack);
13479
+ result.detectedStacks.push(detected);
13480
+ }
13481
+ }
13482
+ result.isMonorepo = this.detectMonorepo(result.detectedStacks, dirs, files);
13483
+ if (result.isMonorepo) {
13484
+ result.workspaceConfigs = this.findWorkspaceConfigs(result.detectedStacks, dirs, files);
13485
+ }
13486
+ this.cachedResults.set(projectRoot, result);
13487
+ return result;
13488
+ }
13489
+ matchSignature(signature, files, _dirs) {
13490
+ const manifestMatch = this.findMatchingManifest(signature.manifestFiles, files);
13491
+ if (!manifestMatch) return null;
13492
+ if (signature.lockFiles.length > 0 && signature.stack === "nodejs") {
13493
+ const hasLockFile = signature.lockFiles.some((lock) => {
13494
+ if (lock.includes("*")) {
13495
+ const regex = new RegExp("^" + lock.replace("*", ".*") + "$");
13496
+ return files.some((f) => regex.test(f));
13497
+ }
13498
+ return files.includes(lock);
13499
+ });
13500
+ if (!hasLockFile && signature.packageManager !== "npm") {
13501
+ return null;
13502
+ }
13503
+ }
13504
+ return {
13505
+ stack: signature.stack,
13506
+ packageManager: signature.packageManager,
13507
+ manifestFile: manifestMatch,
13508
+ dependencies: [],
13509
+ projectPath: ""
13510
+ };
13511
+ }
13512
+ findMatchingManifest(manifests, files) {
13513
+ for (const manifest of manifests) {
13514
+ if (manifest.includes("*")) {
13515
+ const regex = new RegExp("^" + manifest.replace("*", ".*") + "$");
13516
+ const match = files.find((f) => regex.test(f));
13517
+ if (match) return match;
13518
+ } else if (files.includes(manifest)) {
13519
+ return manifest;
13520
+ }
13521
+ }
13522
+ return null;
13523
+ }
13524
+ detectMonorepo(stacks, dirs, files) {
13525
+ if (stacks.length > 1) return true;
13526
+ if (stacks.some((s) => s.packageManager === "pnpm") && files.includes("pnpm-workspace.yaml")) {
13527
+ return true;
13528
+ }
13529
+ for (const stack of stacks) {
13530
+ const indicators = MONOREPO_INDICATORS[stack.packageManager];
13531
+ if (indicators && indicators.some((ind) => files.includes(ind) || dirs.includes(ind))) {
13532
+ return true;
13533
+ }
13534
+ }
13535
+ return false;
13536
+ }
13537
+ findWorkspaceConfigs(stacks, dirs, files) {
13538
+ const configs = [];
13539
+ for (const stack of stacks) {
13540
+ const indicators = MONOREPO_INDICATORS[stack.packageManager];
13541
+ if (indicators) {
13542
+ configs.push(...indicators.filter((ind) => files.includes(ind) || dirs.includes(ind)));
13543
+ }
13544
+ }
13545
+ return [...new Set(configs)];
13546
+ }
13547
+ clearCache() {
13548
+ this.cachedResults.clear();
13549
+ }
13550
+ };
13551
+ var defaultTechStackDetector = new TechStackDetector();
13552
+
13553
+ // src/security-scanner/skill-generator.ts
13554
+ var DEFAULT_OPTIONS = {
13555
+ includeSecrets: true,
13556
+ includeInjection: true,
13557
+ includeConfig: true,
13558
+ includeDependencies: true,
13559
+ severityThreshold: "medium"
13560
+ };
13561
+ var SkillGenerator = class {
13562
+ options;
13563
+ constructor(options = {}) {
13564
+ this.options = { ...DEFAULT_OPTIONS, ...options };
13565
+ }
13566
+ generate(techStack) {
13567
+ const patterns = this.getPatternsForStack(techStack.stack);
13568
+ const content = this.buildSkillContent(techStack, patterns);
13569
+ const targetFiles = this.getTargetFilesForStack(techStack);
13570
+ return {
13571
+ name: `security-scanner-${techStack.stack}`,
13572
+ description: `Security scanner for ${techStack.stack} projects`,
13573
+ version: "1.0.0",
13574
+ techStack: techStack.stack,
13575
+ content,
13576
+ patterns,
13577
+ metadata: {
13578
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
13579
+ confidence: this.calculateConfidence(techStack),
13580
+ targetFiles
13581
+ }
13582
+ };
13583
+ }
13584
+ getPatternsForStack(stack) {
13585
+ const patterns = [];
13586
+ const severityOrder = ["critical", "high", "medium", "low"];
13587
+ const minIndex = severityOrder.indexOf(this.options.severityThreshold);
13588
+ if (this.options.includeSecrets) {
13589
+ const secretPatterns = this.getSecretPatterns(stack);
13590
+ patterns.push(...secretPatterns.filter((p) => severityOrder.indexOf(p.severity) <= minIndex));
13591
+ }
13592
+ if (this.options.includeInjection) {
13593
+ const injectionPatterns = this.getInjectionPatterns(stack);
13594
+ patterns.push(...injectionPatterns.filter((p) => severityOrder.indexOf(p.severity) <= minIndex));
13595
+ }
13596
+ if (this.options.includeConfig) {
13597
+ const configPatterns = this.getConfigPatterns(stack);
13598
+ patterns.push(...configPatterns.filter((p) => severityOrder.indexOf(p.severity) <= minIndex));
13599
+ }
13600
+ return patterns;
13601
+ }
13602
+ getSecretPatterns(stack) {
13603
+ const commonSecrets = {
13604
+ id: "hardcoded-secrets",
13605
+ name: "Hardcoded Secrets",
13606
+ severity: "critical",
13607
+ description: "Detects hardcoded API keys, tokens, passwords, and private keys",
13608
+ patterns: [
13609
+ /(?:api[_-]?key|apikey|api[_-]?secret)[^\w]*[=:]\s*["']([a-zA-Z0-9_-]{20,})["']/gi,
13610
+ /(?:password|passwd|pwd)[^\w]*[=:]\s*["'][^"']{6,64}["']/gi,
13611
+ /(?:secret|token|auth)[^\w]*[=:]\s*["'][a-zA-Z0-9_/-]{20,}["']/gi,
13612
+ /-----BEGIN\s+(?:RSA|DSA|EC|OPENSSH)?\s+PRIVATE\s+KEY-----/g
13613
+ ],
13614
+ fileExtensions: [".ts", ".js", ".py", ".go", ".java", ".rb", ".php", ".env"],
13615
+ falsePositiveMarkers: ["process.env", "process.argv", "process.env.NODE_ENV"],
13616
+ remediation: "Use environment variables or a secrets manager. Never commit secrets to version control."
13617
+ };
13618
+ const stackSpecific = {
13619
+ nodejs: [
13620
+ {
13621
+ id: "npmrc-credentials",
13622
+ name: "npmrc Credentials",
13623
+ severity: "high",
13624
+ description: "Detects .npmrc authentication tokens",
13625
+ patterns: [
13626
+ // eslint-disable-next-line no-control-regex
13627
+ /_auth\s*=\s*[a-zA-Z0-9+/]{20,}/g,
13628
+ /registry\s*=\s*https:\/\/[a-zA-Z0-9.-]+\/auth/g
13629
+ ],
13630
+ fileExtensions: [".npmrc"],
13631
+ falsePositiveMarkers: [],
13632
+ remediation: "Use npm token from environment variable, not hardcoded in .npmrc"
13633
+ }
13634
+ ],
13635
+ python: [
13636
+ {
13637
+ id: "python-secret-env",
13638
+ name: "Python Environment Secrets",
13639
+ severity: "high",
13640
+ description: "Detects secrets loaded via os.environ in Python",
13641
+ patterns: [
13642
+ /os\.environ\[['"](?:API_KEY|SECRET|PASSWORD|TOKEN)/g,
13643
+ /getenv\(['"](?:API_KEY|SECRET|PASSWORD|TOKEN)/g
13644
+ ],
13645
+ fileExtensions: [".py"],
13646
+ falsePositiveMarkers: ["os.environ.get", "getenv(", "os.getenv"],
13647
+ remediation: "Use python-dotenv or environment variable injection at runtime"
13648
+ }
13649
+ ],
13650
+ rust: [
13651
+ {
13652
+ id: "rust-env-secrets",
13653
+ name: "Rust Environment Secrets",
13654
+ severity: "high",
13655
+ description: "Detects env! macro with hardcoded secrets",
13656
+ patterns: [/env!\s*\[['"](?:API_KEY|SECRET|PASSWORD|TOKEN)/g],
13657
+ fileExtensions: [".rs"],
13658
+ falsePositiveMarkers: [],
13659
+ remediation: "Use std::env::var and handle MissingKeyError properly"
13660
+ }
13661
+ ],
13662
+ go: [
13663
+ {
13664
+ id: "go-hardcoded-secret",
13665
+ name: "Go Hardcoded Secrets",
13666
+ severity: "critical",
13667
+ description: "Detects hardcoded secrets in Go source",
13668
+ patterns: [
13669
+ /os\.Getenv\(['"](?:API_KEY|SECRET|PASSWORD|TOKEN)/g,
13670
+ /"[a-zA-Z0-9/+=]{40,}"\s*(?:&&|\|\|)\s*err\s*!=\s*nil/g
13671
+ ],
13672
+ fileExtensions: [".go"],
13673
+ falsePositiveMarkers: ["os.Getenv", "os.LookupEnv"],
13674
+ remediation: "Use viper or os.Getenv with validation"
13675
+ }
13676
+ ],
13677
+ java: [
13678
+ {
13679
+ id: "java-system-getenv",
13680
+ name: "Java System.getenv",
13681
+ severity: "medium",
13682
+ description: "Detects System.getenv() usage which may expose secrets",
13683
+ patterns: [/System\.getenv\(['"](?:API_KEY|SECRET|PASSWORD|TOKEN)/g],
13684
+ fileExtensions: [".java"],
13685
+ falsePositiveMarkers: [],
13686
+ remediation: "Use a secrets manager or environment-specific config"
13687
+ }
13688
+ ],
13689
+ dotnet: [
13690
+ {
13691
+ id: "dotnet-config-secrets",
13692
+ name: ".NET Configuration Secrets",
13693
+ severity: "high",
13694
+ description: "Detects secrets in .NET Configuration managers",
13695
+ patterns: [
13696
+ /ConfigurationManager\.ConnectionStrings/g,
13697
+ /ConfigurationManager\.AppSettings/g
13698
+ ],
13699
+ fileExtensions: [".cs", ".config"],
13700
+ falsePositiveMarkers: [],
13701
+ remediation: "Use Azure Key Vault or user secrets during development"
13702
+ }
13703
+ ]
13704
+ };
13705
+ return [commonSecrets, ...stackSpecific[stack] ?? []];
13706
+ }
13707
+ getInjectionPatterns(stack) {
13708
+ const commonInjection = {
13709
+ id: "command-injection",
13710
+ name: "Command Injection",
13711
+ severity: "critical",
13712
+ description: "Detects shell command injection vulnerabilities",
13713
+ patterns: [
13714
+ /exec\s*\(\s*[`'"].*\$/g,
13715
+ /system\s*\(\s*[`'"].*\$/g,
13716
+ /shell_exec\s*\(\s*[`'"].*\$/g,
13717
+ /exec\s*\(\s*\$/g
13718
+ ],
13719
+ fileExtensions: [".ts", ".js", ".php", ".py", ".rb"],
13720
+ falsePositiveMarkers: ["escapeshellarg", "escapeshellcmd", "sanitize"],
13721
+ remediation: "Use parameterized commands with argument arrays instead of string interpolation."
13722
+ };
13723
+ const stackSpecific = {
13724
+ nodejs: [
13725
+ {
13726
+ id: "eval-injection",
13727
+ name: "Eval Injection",
13728
+ severity: "critical",
13729
+ description: "Detects dangerous eval/Function usage with user input",
13730
+ patterns: [
13731
+ /eval\s*\(\s*(?:req|body|input|params|query)/g,
13732
+ /new\s+Function\s*\(\s*(?:req|body|input|params|query)/g
13733
+ ],
13734
+ fileExtensions: [".ts", ".js"],
13735
+ falsePositiveMarkers: ["JSON.parse"],
13736
+ remediation: "Never eval user input. Use JSON.parse for data, or proper sandboxing."
13737
+ },
13738
+ {
13739
+ id: "sql-injection-template",
13740
+ name: "SQL Injection via Template",
13741
+ severity: "critical",
13742
+ description: "Detects SQL queries built with string concatenation",
13743
+ patterns: [
13744
+ /(?:query|execute|select)\s*\(\s*[`'"].*\+.*(?:req|body|params|query)/gi,
13745
+ /\.query\s*\(\s*`.*\$\{/g
13746
+ ],
13747
+ fileExtensions: [".ts", ".js"],
13748
+ falsePositiveMarkers: ["parameterized", "prepared", "bind"],
13749
+ remediation: "Use parameterized queries or an ORM with proper query building."
13750
+ },
13751
+ {
13752
+ id: "nosql-injection",
13753
+ name: "NoSQL Injection",
13754
+ severity: "high",
13755
+ description: "Detects NoSQL query injection via user input",
13756
+ patterns: [
13757
+ /find\s*\(\s*\{.*\$where/g,
13758
+ /collection\.(?:find|aggregate)\s*\([^)]*\$/g
13759
+ ],
13760
+ fileExtensions: [".ts", ".js"],
13761
+ falsePositiveMarkers: [],
13762
+ remediation: "Sanitize and validate all user input before NoSQL queries."
13763
+ }
13764
+ ],
13765
+ python: [
13766
+ {
13767
+ id: "python-sql-injection",
13768
+ name: "Python SQL Injection",
13769
+ severity: "critical",
13770
+ description: "Detects SQL queries built with string formatting",
13771
+ patterns: [
13772
+ /execute\s*\(\s*f?["'].*%.*/g,
13773
+ /cursor\.execute\s*\([^)]*\+[^)]*\)/g
13774
+ ],
13775
+ fileExtensions: [".py"],
13776
+ falsePositiveMarkers: ["%s", "%d", "?", "parameterized"],
13777
+ remediation: "Use parameterized queries with cursor.execute(query, params)."
13778
+ },
13779
+ {
13780
+ id: "pickle-deserialization",
13781
+ name: "Pickle Deserialization",
13782
+ severity: "critical",
13783
+ description: "Detects insecure pickle deserialization",
13784
+ patterns: [
13785
+ /pickle\.load\s*\(/g,
13786
+ /pickle\.loads\s*\(/g,
13787
+ /unpickle\.load\s*\(/g
13788
+ ],
13789
+ fileExtensions: [".py"],
13790
+ falsePositiveMarkers: [],
13791
+ remediation: "Never unpickle data from untrusted sources. Use JSON or custom serialization."
13792
+ }
13793
+ ],
13794
+ go: [
13795
+ {
13796
+ id: "go-sql-injection",
13797
+ name: "Go SQL Injection",
13798
+ severity: "critical",
13799
+ description: "Detects SQL queries with string concatenation",
13800
+ patterns: [
13801
+ /db\.Query\s*\([^)]*\+[^)]*\)/g,
13802
+ /QueryContext?\s*\([^)]*\+[^)]*\)/g
13803
+ ],
13804
+ fileExtensions: [".go"],
13805
+ falsePositiveMarkers: ["$1", "$2", "?", "params"],
13806
+ remediation: 'Use parameterized queries: db.QueryContext(ctx, "SELECT * FROM users WHERE id=?", userID)'
13807
+ }
13808
+ ],
13809
+ java: [
13810
+ {
13811
+ id: "java-sql-injection",
13812
+ name: "Java SQL Injection",
13813
+ severity: "critical",
13814
+ description: "Detects SQL with string concatenation in JDBC",
13815
+ patterns: [
13816
+ /createStatement\s*\(\s*\).*\.executeQuery\s*\([^)]*\+/g,
13817
+ /Statement\s*\([^)]*\+/g
13818
+ ],
13819
+ fileExtensions: [".java"],
13820
+ falsePositiveMarkers: ["PreparedStatement", "?"],
13821
+ remediation: "Use PreparedStatement with parameters."
13822
+ }
13823
+ ],
13824
+ rust: [
13825
+ {
13826
+ id: "rust-command-injection",
13827
+ name: "Rust Command Injection",
13828
+ severity: "critical",
13829
+ description: "Detects Command::new with string interpolation",
13830
+ patterns: [
13831
+ /Command::new\s*\([^)]*\)\s*\.(?:arg|args)\s*\([^)]*\+/g,
13832
+ /Command::from\s*\(/g
13833
+ ],
13834
+ fileExtensions: [".rs"],
13835
+ falsePositiveMarkers: ["Command::new", "args\\("],
13836
+ remediation: "Use Command::new(array).args(&[...]) to avoid shell injection."
13837
+ }
13838
+ ],
13839
+ dotnet: [
13840
+ {
13841
+ id: "csharp-sql-injection",
13842
+ name: "C# SQL Injection",
13843
+ severity: "critical",
13844
+ description: "Detects SQL with string concatenation in C#",
13845
+ patterns: [
13846
+ /SqlCommand\s*\([^)]*\+[^)]*\)/g,
13847
+ /\.ExecuteQuery\s*\([^)]*\+[^)]*\)/g
13848
+ ],
13849
+ fileExtensions: [".cs"],
13850
+ falsePositiveMarkers: ["parameters.Add", "@", "SqlParameter"],
13851
+ remediation: "Use parameterized queries with SqlParameter."
13852
+ }
13853
+ ]
13854
+ };
13855
+ return [commonInjection, ...stackSpecific[stack] ?? []];
13856
+ }
13857
+ getConfigPatterns(stack) {
13858
+ const commonConfig = [
13859
+ {
13860
+ id: "insecure-tls",
13861
+ name: "Insecure TLS Configuration",
13862
+ severity: "high",
13863
+ description: "Detects disabled TLS verification or weak TLS settings",
13864
+ patterns: [
13865
+ /rejectUnauthorized\s*[:=]\s*false/g,
13866
+ /secure\s*[:=]\s*false/g,
13867
+ /ssl\s*[:=]\s*false/g,
13868
+ /TLS\s*\[\s*['"]?1\.0['"]?\]/gi,
13869
+ /InsecureRequestWarning\.disable/g
13870
+ ],
13871
+ fileExtensions: [".ts", ".js", ".py", ".go", ".java"],
13872
+ falsePositiveMarkers: ["NODE_TLS_REJECT_UNAUTHORIZED"],
13873
+ remediation: "Always verify TLS certificates in production. Use proper certificate stores."
13874
+ },
13875
+ {
13876
+ id: "debug-enabled",
13877
+ name: "Debug Mode Enabled",
13878
+ severity: "medium",
13879
+ description: "Detects debug flags that may expose sensitive information",
13880
+ patterns: [
13881
+ /debug\s*[:=]\s*true/g,
13882
+ /DEBUG\s*[:=]\s*true/g,
13883
+ /development\s*mode/g
13884
+ ],
13885
+ fileExtensions: [".ts", ".js", ".py", ".env", ".json"],
13886
+ falsePositiveMarkers: ['process.env.NODE_ENV !== "production"', "if (process.env.DEBUG)"],
13887
+ remediation: "Disable debug mode in production. Use proper log levels."
13888
+ }
13889
+ ];
13890
+ return commonConfig;
13891
+ }
13892
+ getTargetFilesForStack(techStack) {
13893
+ const filesByStack = {
13894
+ nodejs: [
13895
+ "**/*.ts",
13896
+ "**/*.js",
13897
+ "**/*.json",
13898
+ "**/.env*",
13899
+ "**/package.json",
13900
+ "**/tsconfig.json"
13901
+ ],
13902
+ python: [
13903
+ "**/*.py",
13904
+ "**/requirements*.txt",
13905
+ "**/setup.py",
13906
+ "**/pyproject.toml",
13907
+ "**/.env*"
13908
+ ],
13909
+ rust: ["**/*.rs", "**/Cargo.toml", "**/Cargo.lock"],
13910
+ go: ["**/*.go", "**/go.mod", "**/go.sum"],
13911
+ java: ["**/*.java", "**/pom.xml", "**/build.gradle", "**/*.properties"],
13912
+ dotnet: ["**/*.cs", "**/*.csproj", "**/*.config", "**/appsettings.json"],
13913
+ php: ["**/*.php", "**/.env*", "**/composer.json"],
13914
+ ruby: ["**/*.rb", "**/Gemfile", "**/.env*"],
13915
+ cpp: ["**/*.cpp", "**/*.hpp", "**/CMakeLists.txt"],
13916
+ c: ["**/*.c", "**/*.h"],
13917
+ kotlin: ["**/*.kt", "**/*.kts", "**/build.gradle.kts"],
13918
+ swift: ["**/*.swift", "**/Package.swift"],
13919
+ unknown: ["**/*"]
13920
+ };
13921
+ return filesByStack[techStack.stack] || filesByStack.unknown;
13922
+ }
13923
+ buildSkillContent(techStack, patterns) {
13924
+ const lines = [
13925
+ "---",
13926
+ `name: security-scanner-${techStack.stack}`,
13927
+ `description: |`,
13928
+ ` Auto-generated security scanner for ${techStack.stack} projects.`,
13929
+ ` Scans for secrets, injection vectors, and configuration issues.`,
13930
+ `version: 1.0.0`,
13931
+ "---",
13932
+ "",
13933
+ `# Security Scanner \u2014 ${techStack.stack.toUpperCase()}`,
13934
+ "",
13935
+ `Scans ${techStack.stack} codebase for security vulnerabilities.`,
13936
+ "",
13937
+ "## Scan Targets",
13938
+ "",
13939
+ "### Code Vulnerabilities",
13940
+ patterns.filter((p) => p.fileExtensions.some((ext) => [".ts", ".js", ".py", ".go", ".java", ".cs", ".rs"].includes(ext))).map((p) => `- **${p.name}** (${p.severity}): ${p.description}`).join("\n"),
13941
+ "",
13942
+ "### Configuration Issues",
13943
+ patterns.filter((p) => p.fileExtensions.some((ext) => [".json", ".yaml", ".yml", ".env", ".config"].includes(ext))).map((p) => `- **${p.name}** (${p.severity}): ${p.description}`).join("\n"),
13944
+ "",
13945
+ "## Severity Levels",
13946
+ "",
13947
+ "- **CRITICAL**: Remote code execution, SQL injection, hardcoded secrets",
13948
+ "- **HIGH**: Command injection, XXE, authentication bypass",
13949
+ "- **MEDIUM**: Information disclosure, weak crypto, debug mode",
13950
+ "- **LOW**: Code quality issues, missing headers",
13951
+ "",
13952
+ "## Report Format",
13953
+ "",
13954
+ "```",
13955
+ "## Security Scan Report",
13956
+ "",
13957
+ "### CRITICAL",
13958
+ "1. **[CRITICAL]** `file:line` \u2014 Description",
13959
+ " ```",
13960
+ " // vulnerable code",
13961
+ " ```",
13962
+ " **Remediation**: Fix description",
13963
+ "",
13964
+ "### Summary",
13965
+ "| Severity | Count |",
13966
+ "|----------|-------|",
13967
+ "| Critical | X |",
13968
+ "| High | X |",
13969
+ "| Medium | X |",
13970
+ "| Low | X |",
13971
+ "```",
13972
+ "",
13973
+ "## Remediation",
13974
+ "",
13975
+ patterns.map((p) => `- **${p.name}**: ${p.remediation}`).join("\n")
13976
+ ];
13977
+ return {
13978
+ type: "skill",
13979
+ content: lines.join("\n")
13980
+ };
13981
+ }
13982
+ calculateConfidence(techStack) {
13983
+ let confidence = 0.7;
13984
+ if (techStack.dependencies.length > 0) confidence += 0.1;
13985
+ if (techStack.manifestFile) confidence += 0.1;
13986
+ if (techStack.packageManager !== "unknown") confidence += 0.1;
13987
+ return Math.min(confidence, 1);
13988
+ }
13989
+ };
13990
+ var defaultSkillGenerator = new SkillGenerator();
13991
+ var DEFAULT_SCAN_OPTIONS = {
13992
+ includeSecrets: true,
13993
+ includeInjection: true,
13994
+ includeConfig: true,
13995
+ includeDependencies: true,
13996
+ excludePaths: ["node_modules", "dist", ".git", "coverage", "build", "target"],
13997
+ fileExtensions: [],
13998
+ depth: "standard"
13999
+ };
14000
+ var SecurityScanner = class {
14001
+ options;
14002
+ constructor(options = {}) {
14003
+ this.options = { ...DEFAULT_SCAN_OPTIONS, ...options };
14004
+ }
14005
+ async scan(projectRoot, skill, techStack) {
14006
+ const startTime = Date.now();
14007
+ const findings = [];
14008
+ const errors = [];
14009
+ let scannedFiles = 0;
14010
+ const targetExtensions = this.getTargetExtensions(skill, techStack);
14011
+ const files = await this.gatherFiles(projectRoot, targetExtensions);
14012
+ for (const file of files) {
14013
+ try {
14014
+ const content = await readFile(file, "utf-8");
14015
+ const fileFindings = this.scanFile(content, file, skill.patterns);
14016
+ findings.push(...fileFindings);
14017
+ scannedFiles++;
14018
+ } catch (err) {
14019
+ errors.push(`Failed to read ${file}: ${err}`);
14020
+ }
14021
+ }
14022
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
14023
+ findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
14024
+ const summary = this.calculateSummary(findings);
14025
+ const scanDurationMs = Date.now() - startTime;
14026
+ return {
14027
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14028
+ projectRoot,
14029
+ techStack,
14030
+ findings,
14031
+ summary,
14032
+ scannedFiles,
14033
+ scanDurationMs,
14034
+ errors
14035
+ };
14036
+ }
14037
+ async gatherFiles(root, extensions) {
14038
+ const files = [];
14039
+ const maxDepth = this.options.depth === "quick" ? 2 : this.options.depth === "deep" ? 20 : 5;
14040
+ await this.gatherFilesRecursive(root, files, extensions, 0, maxDepth);
14041
+ return files;
14042
+ }
14043
+ async gatherFilesRecursive(dir, files, extensions, currentDepth, maxDepth) {
14044
+ if (currentDepth > maxDepth) return;
14045
+ try {
14046
+ const entries = await readdir(dir, { withFileTypes: true });
14047
+ for (const entry of entries) {
14048
+ const fullPath = join(dir, entry.name);
14049
+ if (entry.isDirectory()) {
14050
+ if (this.shouldExclude(entry.name)) continue;
14051
+ await this.gatherFilesRecursive(fullPath, files, extensions, currentDepth + 1, maxDepth);
14052
+ } else if (entry.isFile()) {
14053
+ if (extensions.length === 0 || extensions.includes(extname(entry.name))) {
14054
+ files.push(fullPath);
14055
+ }
14056
+ }
14057
+ }
14058
+ } catch {
14059
+ }
14060
+ }
14061
+ shouldExclude(name) {
14062
+ return this.options.excludePaths.some(
14063
+ (exclude) => name === exclude || name.startsWith(exclude + "/") || name.startsWith(exclude + "\\")
14064
+ );
14065
+ }
14066
+ getTargetExtensions(skill, _techStack) {
14067
+ if (this.options.fileExtensions.length > 0) {
14068
+ return this.options.fileExtensions;
14069
+ }
14070
+ const extensions = /* @__PURE__ */ new Set();
14071
+ for (const pattern of skill.patterns) {
14072
+ for (const ext of pattern.fileExtensions) {
14073
+ if (ext.startsWith(".")) {
14074
+ extensions.add(ext.toLowerCase());
14075
+ }
14076
+ }
14077
+ }
14078
+ return [...extensions];
14079
+ }
14080
+ scanFile(content, filePath, patterns) {
14081
+ const findings = [];
14082
+ const lines = content.split("\n");
14083
+ for (const pattern of patterns) {
14084
+ if (!this.matchesCategory(pattern)) continue;
14085
+ for (const regex of pattern.patterns) {
14086
+ regex.lastIndex = 0;
14087
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
14088
+ const line = lines[lineNum];
14089
+ if (!line) continue;
14090
+ if (regex.test(line)) {
14091
+ if (this.isFalsePositive(line, pattern.falsePositiveMarkers)) {
14092
+ continue;
14093
+ }
14094
+ findings.push({
14095
+ id: `${pattern.id}-${filePath}-${lineNum}`,
14096
+ severity: pattern.severity,
14097
+ category: this.getCategoryFromPattern(pattern),
14098
+ title: pattern.name,
14099
+ description: pattern.description,
14100
+ file: relative(process.cwd(), filePath),
14101
+ line: lineNum + 1,
14102
+ snippet: line.trim(),
14103
+ remediation: pattern.remediation,
14104
+ patternId: pattern.id,
14105
+ confidence: "high"
14106
+ });
14107
+ }
14108
+ }
14109
+ }
14110
+ }
14111
+ return findings;
14112
+ }
14113
+ matchesCategory(pattern) {
14114
+ if (pattern.id.includes("secret") || pattern.id.includes("npmrc") || pattern.id.includes("env")) {
14115
+ return this.options.includeSecrets;
14116
+ }
14117
+ if (pattern.id.includes("injection") || pattern.id.includes("sql") || pattern.id.includes("command") || pattern.id.includes("eval")) {
14118
+ return this.options.includeInjection;
14119
+ }
14120
+ if (pattern.id.includes("config") || pattern.id.includes("tls") || pattern.id.includes("debug")) {
14121
+ return this.options.includeConfig;
14122
+ }
14123
+ return true;
14124
+ }
14125
+ getCategoryFromPattern(pattern) {
14126
+ if (pattern.id.includes("secret")) return "secrets";
14127
+ if (pattern.id.includes("injection") || pattern.id.includes("sql") || pattern.id.includes("command")) return "injection";
14128
+ if (pattern.id.includes("config") || pattern.id.includes("tls") || pattern.id.includes("debug")) return "config";
14129
+ if (pattern.id.includes("dependency")) return "dependency";
14130
+ return "filesystem";
14131
+ }
14132
+ isFalsePositive(line, markers) {
14133
+ for (const marker of markers) {
14134
+ if (line.includes(marker)) {
14135
+ return true;
14136
+ }
14137
+ }
14138
+ return false;
14139
+ }
14140
+ calculateSummary(findings) {
14141
+ return {
14142
+ critical: findings.filter((f) => f.severity === "critical").length,
14143
+ high: findings.filter((f) => f.severity === "high").length,
14144
+ medium: findings.filter((f) => f.severity === "medium").length,
14145
+ low: findings.filter((f) => f.severity === "low").length,
14146
+ total: findings.length
14147
+ };
14148
+ }
14149
+ };
14150
+ var defaultSecurityScanner = new SecurityScanner();
14151
+ var DEFAULT_REPORT_OPTIONS = {
14152
+ outputDir: "security-reports",
14153
+ format: "markdown",
14154
+ includeRemediation: true,
14155
+ groupBySeverity: true
14156
+ };
14157
+ var ReportGenerator = class {
14158
+ options;
14159
+ constructor(options = {}) {
14160
+ this.options = { ...DEFAULT_REPORT_OPTIONS, ...options };
14161
+ }
14162
+ async generate(scanResult) {
14163
+ await this.ensureOutputDir();
14164
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
14165
+ const filename = `security-report-${timestamp}.${this.options.format}`;
14166
+ const filepath = join(this.options.outputDir, filename);
14167
+ let content;
14168
+ switch (this.options.format) {
14169
+ case "json":
14170
+ content = this.generateJson(scanResult);
14171
+ break;
14172
+ case "html":
14173
+ content = this.generateHtml(scanResult);
14174
+ break;
14175
+ default:
14176
+ content = this.generateMarkdown(scanResult);
14177
+ }
14178
+ await writeFile(filepath, content, "utf-8");
14179
+ return filepath;
14180
+ }
14181
+ async ensureOutputDir() {
14182
+ try {
14183
+ await stat(this.options.outputDir);
14184
+ } catch {
14185
+ const { mkdir: mkdir10 } = await import('fs/promises');
14186
+ await mkdir10(this.options.outputDir, { recursive: true });
14187
+ }
14188
+ }
14189
+ generateMarkdown(result) {
14190
+ const lines = [];
14191
+ lines.push("# Security Scan Report");
14192
+ lines.push("");
14193
+ lines.push(`**Generated:** ${new Date(result.timestamp).toLocaleString()}`);
14194
+ lines.push(`**Project:** ${result.projectRoot}`);
14195
+ lines.push(`**Tech Stack:** ${result.techStack.stack} (${result.techStack.packageManager})`);
14196
+ lines.push(`**Scanned Files:** ${result.scannedFiles}`);
14197
+ lines.push(`**Scan Duration:** ${result.scanDurationMs}ms`);
14198
+ lines.push("");
14199
+ lines.push("## Summary");
14200
+ lines.push("");
14201
+ lines.push("| Severity | Count |");
14202
+ lines.push("|----------|-------|");
14203
+ lines.push(`| \u{1F534} Critical | ${result.summary.critical} |`);
14204
+ lines.push(`| \u{1F7E0} High | ${result.summary.high} |`);
14205
+ lines.push(`| \u{1F7E1} Medium | ${result.summary.medium} |`);
14206
+ lines.push(`| \u{1F7E2} Low | ${result.summary.low} |`);
14207
+ lines.push(`| **Total** | **${result.summary.total}** |`);
14208
+ lines.push("");
14209
+ if (this.options.groupBySeverity) {
14210
+ const severityGroups = this.groupBySeverity(result.findings);
14211
+ for (const severity of ["critical", "high", "medium", "low"]) {
14212
+ const findings = severityGroups[severity] ?? [];
14213
+ if (findings.length === 0) continue;
14214
+ const emoji = severity === "critical" ? "\u{1F534}" : severity === "high" ? "\u{1F7E0}" : severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
14215
+ lines.push(`## ${emoji} ${severity.toUpperCase()} (${findings.length})`);
14216
+ lines.push("");
14217
+ for (const finding of findings) {
14218
+ lines.push(this.formatFinding(finding));
14219
+ }
14220
+ lines.push("");
14221
+ }
14222
+ } else {
14223
+ const categoryGroups = this.groupByCategory(result.findings);
14224
+ for (const [category, findings] of Object.entries(categoryGroups)) {
14225
+ lines.push(`## ${category.toUpperCase()} (${findings.length})`);
14226
+ lines.push("");
14227
+ for (const finding of findings) {
14228
+ lines.push(this.formatFinding(finding));
14229
+ }
14230
+ lines.push("");
14231
+ }
14232
+ }
14233
+ if (result.errors.length > 0) {
14234
+ lines.push("## Scan Errors");
14235
+ lines.push("");
14236
+ for (const error of result.errors) {
14237
+ lines.push(`- ${error}`);
14238
+ }
14239
+ lines.push("");
14240
+ }
14241
+ lines.push("---");
14242
+ lines.push(`*Report generated by WrongStack Security Scanner*`);
14243
+ return lines.join("\n");
14244
+ }
14245
+ formatFinding(finding) {
14246
+ const lines = [];
14247
+ lines.push(`### ${finding.title}`);
14248
+ lines.push("");
14249
+ lines.push(`**File:** \`${finding.file}${finding.line ? `:${finding.line}` : ""}\``);
14250
+ lines.push(`**Severity:** ${finding.severity.toUpperCase()}`);
14251
+ lines.push(`**Category:** ${finding.category}`);
14252
+ lines.push("");
14253
+ if (finding.snippet) {
14254
+ lines.push("```");
14255
+ lines.push(finding.snippet);
14256
+ lines.push("```");
14257
+ lines.push("");
14258
+ }
14259
+ if (this.options.includeRemediation) {
14260
+ lines.push(`**Remediation:** ${finding.remediation}`);
14261
+ lines.push("");
14262
+ }
14263
+ lines.push("---");
14264
+ lines.push("");
14265
+ return lines.join("\n");
14266
+ }
14267
+ groupBySeverity(findings) {
14268
+ const groups = {
14269
+ critical: [],
14270
+ high: [],
14271
+ medium: [],
14272
+ low: []
14273
+ };
14274
+ for (const finding of findings) {
14275
+ const group = groups[finding.severity];
14276
+ if (group) {
14277
+ group.push(finding);
14278
+ }
14279
+ }
14280
+ return groups;
14281
+ }
14282
+ groupByCategory(findings) {
14283
+ const groups = {};
14284
+ for (const finding of findings) {
14285
+ const group = groups[finding.category] ?? (groups[finding.category] = []);
14286
+ group.push(finding);
14287
+ }
14288
+ return groups;
14289
+ }
14290
+ generateJson(result) {
14291
+ return JSON.stringify(
14292
+ {
14293
+ meta: {
14294
+ timestamp: result.timestamp,
14295
+ projectRoot: result.projectRoot,
14296
+ techStack: result.techStack,
14297
+ scannedFiles: result.scannedFiles,
14298
+ scanDurationMs: result.scanDurationMs
14299
+ },
14300
+ summary: result.summary,
14301
+ findings: result.findings,
14302
+ errors: result.errors
14303
+ },
14304
+ null,
14305
+ 2
14306
+ );
14307
+ }
14308
+ generateHtml(result) {
14309
+ const severityColors = {
14310
+ critical: "#dc2626",
14311
+ high: "#ea580c",
14312
+ medium: "#ca8a04",
14313
+ low: "#16a34a"
14314
+ };
14315
+ const rows = result.findings.map(
14316
+ (f) => `
14317
+ <tr>
14318
+ <td style="color: ${severityColors[f.severity]}; font-weight: bold;">${f.severity.toUpperCase()}</td>
14319
+ <td>${f.category}</td>
14320
+ <td>${f.title}</td>
14321
+ <td><code>${f.file}${f.line ? `:${f.line}` : ""}</code></td>
14322
+ <td>${f.remediation}</td>
14323
+ </tr>
14324
+ `
14325
+ ).join("");
14326
+ return `
14327
+ <!DOCTYPE html>
14328
+ <html>
14329
+ <head>
14330
+ <title>Security Scan Report - ${new Date(result.timestamp).toLocaleDateString()}</title>
14331
+ <style>
14332
+ body { font-family: system-ui, sans-serif; margin: 2rem; }
14333
+ table { border-collapse: collapse; width: 100%; }
14334
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
14335
+ th { background: #f5f5f5; }
14336
+ code { background: #f0f0f0; padding: 2px 4px; }
14337
+ </style>
14338
+ </head>
14339
+ <body>
14340
+ <h1>Security Scan Report</h1>
14341
+ <p><strong>Generated:</strong> ${new Date(result.timestamp).toLocaleString()}</p>
14342
+ <p><strong>Project:</strong> ${result.projectRoot}</p>
14343
+ <p><strong>Tech Stack:</strong> ${result.techStack.stack} (${result.techStack.packageManager})</p>
14344
+
14345
+ <h2>Summary</h2>
14346
+ <ul>
14347
+ <li>\u{1F534} Critical: ${result.summary.critical}</li>
14348
+ <li>\u{1F7E0} High: ${result.summary.high}</li>
14349
+ <li>\u{1F7E1} Medium: ${result.summary.medium}</li>
14350
+ <li>\u{1F7E2} Low: ${result.summary.low}</li>
14351
+ </ul>
14352
+
14353
+ <h2>Findings</h2>
14354
+ <table>
14355
+ <tr>
14356
+ <th>Severity</th>
14357
+ <th>Category</th>
14358
+ <th>Title</th>
14359
+ <th>Location</th>
14360
+ <th>Remediation</th>
14361
+ </tr>
14362
+ ${rows}
14363
+ </table>
14364
+ </body>
14365
+ </html>
14366
+ `.trim();
14367
+ }
14368
+ };
14369
+ var defaultReportGenerator = new ReportGenerator();
14370
+ var DEFAULT_OPTIONS2 = {
14371
+ gitignorePath: ".gitignore",
14372
+ entries: ["security-reports/", "security-reports/*"]
14373
+ };
14374
+ var GitignoreUpdater = class {
14375
+ options;
14376
+ constructor(options = {}) {
14377
+ this.options = { ...DEFAULT_OPTIONS2, ...options };
14378
+ }
14379
+ async update() {
14380
+ const added = [];
14381
+ const existing = [];
14382
+ const errors = [];
14383
+ try {
14384
+ const content = await readFile(this.options.gitignorePath, "utf-8");
14385
+ const lines = new Set(content.split(/\r?\n/).map((l) => l.trim()));
14386
+ for (const entry of this.options.entries) {
14387
+ if (lines.has(entry)) {
14388
+ existing.push(entry);
14389
+ } else {
14390
+ lines.add(entry);
14391
+ added.push(entry);
14392
+ }
14393
+ }
14394
+ if (added.length > 0) {
14395
+ const newContent = [...lines].filter(Boolean).join("\n") + "\n";
14396
+ await writeFile(this.options.gitignorePath, newContent, "utf-8");
14397
+ }
14398
+ } catch (err) {
14399
+ if (err.code === "ENOENT") {
14400
+ const content = this.options.entries.join("\n") + "\n";
14401
+ await writeFile(this.options.gitignorePath, content, "utf-8");
14402
+ added.push(...this.options.entries);
14403
+ } else {
14404
+ errors.push(`Failed to update .gitignore: ${err}`);
14405
+ }
14406
+ }
14407
+ return { added, existing, errors };
14408
+ }
14409
+ async isEntryIgnored(entry) {
14410
+ try {
14411
+ const content = await readFile(this.options.gitignorePath, "utf-8");
14412
+ const lines = content.split(/\r?\n/).map((l) => l.trim());
14413
+ return lines.includes(entry);
14414
+ } catch {
14415
+ return false;
14416
+ }
14417
+ }
14418
+ };
14419
+ var defaultGitignoreUpdater = new GitignoreUpdater();
14420
+ var NETWORK_ERR_RE2 = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
14421
+ var SecurityScannerOrchestrator = class {
14422
+ constructor(retryPolicy, errorHandler) {
14423
+ this.retryPolicy = retryPolicy;
14424
+ this.errorHandler = errorHandler;
14425
+ }
14426
+ retryPolicy;
14427
+ errorHandler;
14428
+ detector = defaultTechStackDetector;
14429
+ reportGenerator = defaultReportGenerator;
14430
+ gitignoreUpdater = defaultGitignoreUpdater;
14431
+ /**
14432
+ * Wraps provider.complete with retry logic using the injected RetryPolicy.
14433
+ */
14434
+ async completeWithRetry(provider, request, abortController, attempt = 0) {
14435
+ const signal = abortController.signal;
14436
+ try {
14437
+ return await provider.complete(request, { signal });
14438
+ } catch (err) {
14439
+ if (signal.aborted) throw err;
14440
+ const isProviderErr = err instanceof ProviderError;
14441
+ const policy = this.retryPolicy;
14442
+ const errAsErr = isProviderErr ? err : err instanceof Error ? err : new Error(String(err));
14443
+ if (!policy || !isProviderErr && !NETWORK_ERR_RE2.test(errAsErr.message)) {
14444
+ throw err;
14445
+ }
14446
+ const canRetry = policy.shouldRetry(errAsErr, attempt);
14447
+ if (!canRetry) throw err;
14448
+ if (this.errorHandler) {
14449
+ const classified = this.errorHandler.classify(err);
14450
+ if (!classified.retryable) throw err;
14451
+ }
14452
+ const delay = Math.round(policy.delayMs(attempt));
14453
+ const status = isProviderErr ? err.status : 0;
14454
+ console.warn(`[SecurityScanner] retry ${attempt + 1} after ${delay}ms (status=${status}) \u2014 ${errAsErr.message}`);
14455
+ await new Promise((resolve5) => setTimeout(resolve5, delay));
14456
+ return this.completeWithRetry(provider, request, abortController, attempt + 1);
14457
+ }
14458
+ }
14459
+ /**
14460
+ * Run full security scan with LLM assistance.
14461
+ * Accepts a full Context (active agent run) or just provider+model (pre-agent session).
14462
+ */
14463
+ async run(ctx, options) {
14464
+ const { projectRoot, reportOptions, skipGitignore, model: explicitModel } = options;
14465
+ const provider = "provider" in ctx ? ctx.provider : ctx;
14466
+ const model = explicitModel ?? ("model" in ctx ? ctx.model : void 0);
14467
+ const detectionResult = await this.detector.detect(projectRoot);
14468
+ if (detectionResult.detectedStacks.length === 0) {
14469
+ throw new Error(`No supported tech stack detected in ${projectRoot}`);
14470
+ }
14471
+ const techStack = detectionResult.detectedStacks[0];
14472
+ const generatedSkill = await this.generateSkillLLM(provider, model, projectRoot, techStack);
14473
+ const scanResult = await this.scanWithLLM(provider, model, projectRoot, generatedSkill, techStack, options);
14474
+ const synthesizedReport = await this.synthesizeReportLLM(provider, model, projectRoot, techStack, scanResult);
14475
+ const reportPath = await this.writeReport(synthesizedReport, reportOptions);
14476
+ let gitignoreResult;
14477
+ if (!skipGitignore) {
14478
+ gitignoreResult = await this.gitignoreUpdater.update();
14479
+ }
14480
+ return {
14481
+ detectionResult,
14482
+ generatedSkill,
14483
+ scanResult,
14484
+ reportPath,
14485
+ synthesizedReport,
14486
+ gitignoreResult
14487
+ };
14488
+ }
14489
+ /**
14490
+ * Generate a project-specific security skill using LLM.
14491
+ * The LLM analyzes the project structure and creates tailored security patterns.
14492
+ */
14493
+ async generateSkillLLM(provider, model, projectRoot, techStack) {
14494
+ const projectInfo = await this.gatherProjectInfo(projectRoot, techStack);
14495
+ const prompt = `You are a security expert generating a customized security scanning skill for a specific project.
14496
+
14497
+ Analyze the following project and generate a detailed security skill with:
14498
+ 1. Project-specific vulnerability patterns based on the tech stack and structure
14499
+ 2. Language/framework specific security concerns
14500
+ 3. Common attack vectors for this type of application
14501
+ 4. File patterns to scan
14502
+
14503
+ ## Project Information:
14504
+ ${projectInfo}
14505
+
14506
+ ## Tech Stack:
14507
+ - Language: ${techStack.stack}
14508
+ - Package Manager: ${techStack.packageManager}
14509
+ - Manifest: ${techStack.manifestFile}
14510
+
14511
+ ## Dependencies (first 20):
14512
+ ${techStack.dependencies.slice(0, 20).map((d) => `- ${d.name}@${d.version}`).join("\n")}
14513
+
14514
+ ## Your Task:
14515
+ Generate a JSON security skill with the following structure:
14516
+ {
14517
+ "name": "security-scanner-${techStack.stack}",
14518
+ "description": "Custom security scanner for this project",
14519
+ "techStack": "${techStack.stack}",
14520
+ "patterns": [
14521
+ {
14522
+ "id": "unique-pattern-id",
14523
+ "name": "Pattern Name",
14524
+ "severity": "critical|high|medium|low",
14525
+ "description": "What this detects",
14526
+ "fileExtensions": [".ts", ".js"],
14527
+ "remediation": "How to fix"
14528
+ }
14529
+ ],
14530
+ "targetFiles": ["**/*.ts", "**/*.js"],
14531
+ "scanInstructions": "Detailed instructions for scanning this codebase"
14532
+ }
14533
+
14534
+ Focus on:
14535
+ - ${techStack.stack === "nodejs" ? "Node.js specific: eval, prototype pollution, npm script injection, express middleware issues, passport.js misconfigs" : ""}
14536
+ - ${techStack.stack === "python" ? "Python specific: pickle deserialization, SQL injection in ORMs, template injection, insecure Django/Flask settings" : ""}
14537
+ - Common: hardcoded secrets, SQL injection, command injection, XSS, path traversal, XXE
14538
+
14539
+ Return ONLY the JSON object, no markdown, no explanation.`;
14540
+ const request = {
14541
+ model: model ?? "unknown",
14542
+ system: [{ type: "text", text: "You are a security expert. Return ONLY valid JSON." }],
14543
+ messages: [{ role: "user", content: prompt }],
14544
+ maxTokens: 4096
14545
+ };
14546
+ try {
14547
+ const abortController = new AbortController();
14548
+ const response = await this.completeWithRetry(provider, request, abortController);
14549
+ const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
14550
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
14551
+ if (jsonMatch) {
14552
+ const sanitized = sanitizeJsonString(jsonMatch[0]) || jsonMatch[0];
14553
+ const skillData = JSON.parse(sanitized);
14554
+ return {
14555
+ name: skillData.name || `security-scanner-${techStack.stack}`,
14556
+ description: skillData.description || `Security scanner for ${techStack.stack}`,
14557
+ version: "1.0.0",
14558
+ techStack: techStack.stack,
14559
+ content: { type: "skill", content: JSON.stringify(skillData, null, 2) },
14560
+ patterns: skillData.patterns || [],
14561
+ metadata: {
14562
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14563
+ confidence: 0.85,
14564
+ targetFiles: skillData.targetFiles || []
14565
+ }
14566
+ };
14567
+ }
14568
+ } catch (err) {
14569
+ console.error("LLM skill generation failed, using fallback:", err);
14570
+ }
14571
+ return this.generateFallbackSkill(techStack);
14572
+ }
14573
+ /**
14574
+ * Scan code using LLM with the generated skill as context.
14575
+ * The LLM analyzes files and reports security findings.
14576
+ */
14577
+ async scanWithLLM(provider, model, projectRoot, skill, techStack, options) {
14578
+ const startTime = Date.now();
14579
+ const findings = [];
14580
+ const errors = [];
14581
+ let scannedFiles = 0;
14582
+ const files = await this.gatherFiles(projectRoot, skill.metadata.targetFiles, options.scanOptions?.depth || "standard");
14583
+ const batchSize = 10;
14584
+ for (let i = 0; i < files.length; i += batchSize) {
14585
+ const batch = files.slice(i, i + batchSize);
14586
+ const batchFindings = await this.scanFileBatchLLM(provider, model, batch, skill, techStack);
14587
+ findings.push(...batchFindings);
14588
+ scannedFiles += batch.length;
14589
+ }
14590
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
14591
+ findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
14592
+ const summary = {
14593
+ critical: findings.filter((f) => f.severity === "critical").length,
14594
+ high: findings.filter((f) => f.severity === "high").length,
14595
+ medium: findings.filter((f) => f.severity === "medium").length,
14596
+ low: findings.filter((f) => f.severity === "low").length,
14597
+ total: findings.length
14598
+ };
14599
+ return {
14600
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14601
+ projectRoot,
14602
+ techStack,
14603
+ findings,
14604
+ summary,
14605
+ scannedFiles,
14606
+ scanDurationMs: Date.now() - startTime,
14607
+ errors
14608
+ };
14609
+ }
14610
+ /**
14611
+ * Scan a batch of files using LLM.
14612
+ */
14613
+ async scanFileBatchLLM(provider, model, files, skill, techStack) {
14614
+ const fileContents = [];
14615
+ for (const file of files) {
14616
+ try {
14617
+ const content = await readFile(file, "utf-8");
14618
+ const relativePath = relative(process.cwd(), file);
14619
+ fileContents.push(`
14620
+ === ${relativePath} ===
14621
+ ${content.slice(0, 2e3)}`);
14622
+ } catch {
14623
+ }
14624
+ }
14625
+ if (fileContents.length === 0) return [];
14626
+ const prompt = `You are a security expert analyzing code for vulnerabilities.
14627
+
14628
+ ## Security Patterns to Detect (from generated skill):
14629
+ ${skill.patterns.map((p) => `- ${p.name} (${p.severity}): ${p.description}`).join("\n")}
14630
+
14631
+ ## Files to Analyze:
14632
+ ${fileContents.join("\n")}
14633
+
14634
+ ## Your Task:
14635
+ Analyze each file for security vulnerabilities matching the patterns above.
14636
+ For each finding, provide:
14637
+ 1. File path (relative path from === markers)
14638
+ 2. Line number if identifiable
14639
+ 3. Severity (critical/high/medium/low)
14640
+ 4. Category (secrets/injection/config/dependency)
14641
+ 5. Description of the issue
14642
+ 6. Code snippet showing the vulnerability
14643
+ 7. Remediation steps
14644
+
14645
+ Return a JSON array of findings:
14646
+ [
14647
+ {
14648
+ "file": "path/to/file.ts",
14649
+ "line": 42,
14650
+ "severity": "high",
14651
+ "category": "injection",
14652
+ "title": "SQL Injection Risk",
14653
+ "description": "...",
14654
+ "snippet": "actual code...",
14655
+ "remediation": "..."
14656
+ }
14657
+ ]
14658
+
14659
+ Return ONLY the JSON array. If no issues found, return [].`;
14660
+ try {
14661
+ const request = {
14662
+ model: model ?? "unknown",
14663
+ system: [{ type: "text", text: "You are a security expert. Return ONLY valid JSON." }],
14664
+ messages: [{ role: "user", content: prompt }],
14665
+ maxTokens: 4096
14666
+ };
14667
+ const abortController = new AbortController();
14668
+ const response = await this.completeWithRetry(provider, request, abortController);
14669
+ const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
14670
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
14671
+ if (jsonMatch) {
14672
+ const sanitized = sanitizeJsonString(jsonMatch[0]) || jsonMatch[0];
14673
+ const parsed = JSON.parse(sanitized);
14674
+ return parsed.map((item, idx) => ({
14675
+ id: `llm-finding-${idx}-${Date.now()}`,
14676
+ severity: item.severity,
14677
+ category: item.category || "injection",
14678
+ title: item.title,
14679
+ description: item.description,
14680
+ file: item.file,
14681
+ line: item.line,
14682
+ snippet: item.snippet,
14683
+ remediation: item.remediation,
14684
+ patternId: "llm-analysis",
14685
+ confidence: "high"
14686
+ }));
14687
+ }
14688
+ } catch (err) {
14689
+ console.error("LLM scan batch failed:", err);
14690
+ }
14691
+ return [];
14692
+ }
14693
+ /**
14694
+ * Synthesize a comprehensive security report using LLM.
14695
+ */
14696
+ async synthesizeReportLLM(provider, model, projectRoot, techStack, scanResult) {
14697
+ const prompt = `You are a security expert writing a comprehensive security report.
14698
+
14699
+ ## Scan Results:
14700
+ - Scanned Files: ${scanResult.scannedFiles}
14701
+ - Total Findings: ${scanResult.summary.total}
14702
+ - Critical: ${scanResult.summary.critical}
14703
+ - High: ${scanResult.summary.high}
14704
+ - Medium: ${scanResult.summary.medium}
14705
+ - Low: ${scanResult.summary.low}
14706
+
14707
+ ## Detailed Findings:
14708
+ ${scanResult.findings.map((f, i) => `
14709
+ ${i + 1}. [${f.severity.toUpperCase()}] ${f.title}
14710
+ File: ${f.file}${f.line ? `:${f.line}` : ""}
14711
+ Category: ${f.category}
14712
+ Description: ${f.description}
14713
+ ${f.snippet ? `Code: \`\`\`
14714
+ ${f.snippet}
14715
+ \`\`\`` : ""}
14716
+ Remediation: ${f.remediation}
14717
+ `).join("\n")}
14718
+
14719
+ ## Project:
14720
+ - Root: ${projectRoot}
14721
+ - Tech Stack: ${techStack.stack} (${techStack.packageManager})
14722
+
14723
+ ## Your Task:
14724
+ Write a comprehensive markdown security report with:
14725
+ 1. Executive Summary (overall security posture)
14726
+ 2. Critical Issues (with detailed analysis and remediation)
14727
+ 3. High Priority Issues
14728
+ 4. Medium Priority Issues
14729
+ 5. Low Priority / Informational
14730
+ 6. Security Recommendations (prioritized action items)
14731
+ 7. Summary Table
14732
+
14733
+ Format with proper markdown, emojis for severity, and actionable remediation steps.
14734
+
14735
+ Be specific about the vulnerabilities found and how to fix them.`;
14736
+ try {
14737
+ const request = {
14738
+ model: model ?? "unknown",
14739
+ system: [{ type: "text", text: "You are a security expert writing detailed reports." }],
14740
+ messages: [{ role: "user", content: prompt }],
14741
+ maxTokens: 8192
14742
+ };
14743
+ const abortController = new AbortController();
14744
+ const response = await this.completeWithRetry(provider, request, abortController);
14745
+ return response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
14746
+ } catch (err) {
14747
+ console.error("LLM report synthesis failed:", err);
14748
+ return this.generateBasicReport(projectRoot, techStack, scanResult);
14749
+ }
14750
+ }
14751
+ /**
14752
+ * Generate a basic fallback report when LLM synthesis fails.
14753
+ */
14754
+ generateBasicReport(projectRoot, techStack, scanResult) {
14755
+ const lines = [];
14756
+ lines.push("# Security Scan Report");
14757
+ lines.push("");
14758
+ lines.push(`**Generated:** ${new Date(scanResult.timestamp).toLocaleString()}`);
14759
+ lines.push(`**Project:** ${projectRoot}`);
14760
+ lines.push(`**Tech Stack:** ${techStack.stack} (${techStack.packageManager})`);
14761
+ lines.push(`**Scanned Files:** ${scanResult.scannedFiles}`);
14762
+ lines.push("");
14763
+ lines.push("## Summary");
14764
+ lines.push("");
14765
+ lines.push("| Severity | Count |");
14766
+ lines.push("|----------|-------|");
14767
+ lines.push(`| \u{1F534} Critical | ${scanResult.summary.critical} |`);
14768
+ lines.push(`| \u{1F7E0} High | ${scanResult.summary.high} |`);
14769
+ lines.push(`| \u{1F7E1} Medium | ${scanResult.summary.medium} |`);
14770
+ lines.push(`| \u{1F7E2} Low | ${scanResult.summary.low} |`);
14771
+ lines.push("");
14772
+ for (const finding of scanResult.findings) {
14773
+ const emoji = finding.severity === "critical" ? "\u{1F534}" : finding.severity === "high" ? "\u{1F7E0}" : finding.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
14774
+ lines.push(`## ${emoji} ${finding.title}`);
14775
+ lines.push("");
14776
+ lines.push(`**File:** \`${finding.file}${finding.line ? `:${finding.line}` : ""}\``);
14777
+ lines.push(`**Severity:** ${finding.severity.toUpperCase()}`);
14778
+ lines.push(`**Category:** ${finding.category}`);
14779
+ lines.push("");
14780
+ if (finding.snippet) {
14781
+ lines.push("```");
14782
+ lines.push(finding.snippet);
14783
+ lines.push("```");
14784
+ lines.push("");
14785
+ }
14786
+ lines.push(`**Remediation:** ${finding.remediation}`);
14787
+ lines.push("");
14788
+ }
14789
+ return lines.join("\n");
14790
+ }
14791
+ /**
14792
+ * Write synthesized report to file.
14793
+ */
14794
+ async writeReport(content, reportOptions) {
14795
+ const outputDir = reportOptions?.outputDir || "security-reports";
14796
+ const format = reportOptions?.format || "markdown";
14797
+ try {
14798
+ await mkdir(outputDir, { recursive: true });
14799
+ } catch {
14800
+ }
14801
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
14802
+ const filename = `security-report-${timestamp}.${format}`;
14803
+ const filepath = join(outputDir, filename);
14804
+ await writeFile(filepath, content, "utf-8");
14805
+ return filepath;
14806
+ }
14807
+ /**
14808
+ * Gather project info for skill generation.
14809
+ */
14810
+ async gatherProjectInfo(projectRoot, techStack) {
14811
+ const info = [];
14812
+ const keyFiles = [
14813
+ "package.json",
14814
+ "tsconfig.json",
14815
+ ".env.example",
14816
+ "README.md",
14817
+ "CONTRIBUTING.md"
14818
+ ];
14819
+ for (const file of keyFiles) {
14820
+ try {
14821
+ const content = await readFile(join(projectRoot, file), "utf-8");
14822
+ const displayName = file === "README.md" || file === "CONTRIBUTING.md" ? "README" : file;
14823
+ info.push(`
14824
+ --- ${displayName} ---
14825
+ ${content.slice(0, 1e3)}`);
14826
+ } catch {
14827
+ }
14828
+ }
14829
+ try {
14830
+ const entries = await readdir(projectRoot, { withFileTypes: true });
14831
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).slice(0, 20);
14832
+ info.push(`
14833
+ --- Project Directories ---
14834
+ ${dirs.join(", ")}`);
14835
+ } catch {
14836
+ }
14837
+ return info.join("\n");
14838
+ }
14839
+ /**
14840
+ * Gather files to scan based on patterns.
14841
+ */
14842
+ async gatherFiles(root, patterns, depth) {
14843
+ const files = [];
14844
+ const maxDepth = depth === "quick" ? 2 : depth === "deep" ? 20 : 5;
14845
+ const extensions = [".ts", ".js", ".jsx", ".tsx", ".py", ".go", ".java", ".cs", ".rs"];
14846
+ await this.gatherFilesRecursive(root, files, extensions, 0, maxDepth);
14847
+ return files;
14848
+ }
14849
+ async gatherFilesRecursive(dir, files, extensions, currentDepth, maxDepth) {
14850
+ if (currentDepth > maxDepth) return;
14851
+ try {
14852
+ const entries = await readdir(dir, { withFileTypes: true });
14853
+ for (const entry of entries) {
14854
+ if (entry.isDirectory()) {
14855
+ const name = entry.name;
14856
+ if (name === "node_modules" || name === "dist" || name === "build" || name === ".git" || name === "coverage" || name.startsWith(".")) continue;
14857
+ await this.gatherFilesRecursive(
14858
+ join(dir, entry.name),
14859
+ files,
14860
+ extensions,
14861
+ currentDepth + 1,
14862
+ maxDepth
14863
+ );
14864
+ } else if (entry.isFile()) {
14865
+ const ext = entry.name.lastIndexOf(".");
14866
+ if (ext > 0 && extensions.includes(entry.name.slice(ext))) {
14867
+ files.push(join(dir, entry.name));
14868
+ }
14869
+ }
14870
+ }
14871
+ } catch {
14872
+ }
14873
+ }
14874
+ /**
14875
+ * Generate fallback skill when LLM fails.
14876
+ */
14877
+ generateFallbackSkill(techStack) {
14878
+ return {
14879
+ name: `security-scanner-${techStack.stack}`,
14880
+ description: `Security scanner for ${techStack.stack} projects`,
14881
+ version: "1.0.0",
14882
+ techStack: techStack.stack,
14883
+ content: { type: "skill", content: "Fallback static skill" },
14884
+ patterns: [
14885
+ {
14886
+ id: "hardcoded-secrets",
14887
+ name: "Hardcoded Secrets",
14888
+ severity: "critical",
14889
+ description: "Detects hardcoded API keys, tokens, passwords",
14890
+ patterns: [],
14891
+ fileExtensions: [".ts", ".js", ".env"],
14892
+ falsePositiveMarkers: [],
14893
+ remediation: "Use environment variables"
14894
+ }
14895
+ ],
14896
+ metadata: {
14897
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14898
+ confidence: 0.5,
14899
+ targetFiles: [`**/*.${techStack.stack === "nodejs" ? "ts" : techStack.stack === "python" ? "py" : "ts"}`]
14900
+ }
14901
+ };
14902
+ }
14903
+ /**
14904
+ * Quick scan - legacy compatibility.
14905
+ * NOTE: This won't use LLM as it doesn't have access to ctx.
14906
+ */
14907
+ async quickScan(projectRoot) {
14908
+ const detectionResult = await this.detector.detect(projectRoot);
14909
+ if (detectionResult.detectedStacks.length === 0) {
14910
+ throw new Error(`No supported tech stack detected in ${projectRoot}`);
14911
+ }
14912
+ const techStack = detectionResult.detectedStacks[0];
14913
+ return {
14914
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14915
+ projectRoot,
14916
+ techStack,
14917
+ findings: [],
14918
+ summary: { critical: 0, high: 0, medium: 0, low: 0, total: 0 },
14919
+ scannedFiles: 0,
14920
+ scanDurationMs: 0,
14921
+ errors: ["Quick scan without LLM context is not fully supported. Use run(ctx, options) for full scan."]
14922
+ };
14923
+ }
14924
+ };
14925
+ var defaultOrchestrator = new SecurityScannerOrchestrator();
14926
+ function createSecuritySlashCommand() {
14927
+ return {
14928
+ name: "security",
14929
+ description: "Security scanning commands: scan, audit, report",
14930
+ argsHint: "[scan|audit|report] [options]",
14931
+ help: `
14932
+ # /security \u2014 Security Scanner
14933
+
14934
+ Security scanning with automatic tech stack detection.
14935
+
14936
+ ## Commands
14937
+
14938
+ ### /security scan [options]
14939
+ Run a full security scan on the current project.
14940
+ Options:
14941
+ --depth quick|standard|deep Scan depth (default: standard)
14942
+ --format markdown|json|html Report format (default: markdown)
14943
+
14944
+ ### /security audit
14945
+ Run dependency audit + security scan.
14946
+ Checks for known vulnerabilities in dependencies.
14947
+
14948
+ ### /security report [id]
14949
+ Show a previous security report.
14950
+ /security report - List available reports
14951
+ /security report <id> - Show specific report
14952
+
14953
+ ## Examples
14954
+
14955
+ /security scan
14956
+ /security scan --depth deep --format html
14957
+ /security audit
14958
+ /security report
14959
+ /security report 2025-01-15
14960
+ `,
14961
+ async run(args, ctx) {
14962
+ const parts = args.trim().split(/\s+/);
14963
+ const subcommand = parts[0] || "";
14964
+ const subArgs = parts.slice(1).join(" ");
14965
+ switch (subcommand) {
14966
+ case "scan":
14967
+ return handleScan(subArgs, ctx);
14968
+ case "audit":
14969
+ return handleAudit(ctx);
14970
+ case "report":
14971
+ return handleReport(subArgs);
14972
+ default:
14973
+ return { message: getHelpMessage() };
14974
+ }
14975
+ }
14976
+ };
14977
+ }
14978
+ async function handleScan(args, ctx) {
14979
+ const options = parseArgs(args);
14980
+ const projectRoot = ctx.projectRoot || ctx.cwd || process.cwd();
14981
+ try {
14982
+ const result = await defaultOrchestrator.run(ctx, {
14983
+ projectRoot,
14984
+ scanOptions: {
14985
+ depth: options.depth || "standard",
14986
+ includeSecrets: true,
14987
+ includeInjection: true,
14988
+ includeConfig: true
14989
+ },
14990
+ reportOptions: {
14991
+ format: options.format || "markdown"
14992
+ }
14993
+ });
14994
+ const summary = result.scanResult.summary;
14995
+ const status = summary.total === 0 ? "\u2705 No issues found" : `\u26A0\uFE0F Found ${summary.total} issues`;
14996
+ const reportContent = result.synthesizedReport || `# Security Scan Complete
14997
+
14998
+ **Project:** ${projectRoot}
14999
+ **Tech Stack:** ${result.detectionResult.detectedStacks[0]?.stack || "unknown"}
15000
+ **Scanned Files:** ${result.scanResult.scannedFiles}
15001
+ **Duration:** ${result.scanResult.scanDurationMs}ms
15002
+
15003
+ ## Summary
15004
+
15005
+ | Severity | Count |
15006
+ |----------|-------|
15007
+ | \u{1F534} Critical | ${summary.critical} |
15008
+ | \u{1F7E0} High | ${summary.high} |
15009
+ | \u{1F7E1} Medium | ${summary.medium} |
15010
+ | \u{1F7E2} Low | ${summary.low} |
15011
+
15012
+ **Status:** ${status}
15013
+
15014
+ **Report:** ${result.reportPath}
15015
+ `;
15016
+ return {
15017
+ message: reportContent,
15018
+ metadata: {
15019
+ scanResult: result.scanResult,
15020
+ reportPath: result.reportPath,
15021
+ techStack: result.detectionResult.detectedStacks[0]
15022
+ }
15023
+ };
15024
+ } catch (error) {
15025
+ return { message: `\u274C Scan failed: ${error}` };
15026
+ }
15027
+ }
15028
+ async function handleAudit(ctx) {
15029
+ const projectRoot = ctx.projectRoot || ctx.cwd || process.cwd();
15030
+ try {
15031
+ const result = await defaultOrchestrator.run(ctx, {
15032
+ projectRoot,
15033
+ reportOptions: { format: "markdown" }
15034
+ });
15035
+ const depIssues = result.scanResult.summary.critical + result.scanResult.summary.high;
15036
+ const reportContent = result.synthesizedReport || `
15037
+ # Security Audit Complete
15038
+
15039
+ **Project:** ${projectRoot}
15040
+ **Tech Stack:** ${result.detectionResult.detectedStacks[0]?.stack || "unknown"}
15041
+
15042
+ ## Dependency Health
15043
+
15044
+ | Status | Count |
15045
+ |--------|-------|
15046
+ | Critical Issues | ${result.scanResult.summary.critical} |
15047
+ | High Priority | ${result.scanResult.summary.high} |
15048
+ | Medium Priority | ${result.scanResult.summary.medium} |
15049
+ | Low Priority | ${result.scanResult.summary.low} |
15050
+
15051
+ ${depIssues === 0 ? "\u2705 No known vulnerabilities detected" : `\u26A0\uFE0F ${depIssues} vulnerabilities need attention`}
15052
+
15053
+ **Full Report:** ${result.reportPath}
15054
+ `;
15055
+ return {
15056
+ message: reportContent,
15057
+ metadata: {
15058
+ scanResult: result.scanResult,
15059
+ reportPath: result.reportPath
15060
+ }
15061
+ };
15062
+ } catch (error) {
15063
+ return { message: `\u274C Audit failed: ${error}` };
15064
+ }
15065
+ }
15066
+ async function handleReport(reportId) {
15067
+ const reportsDir = "security-reports";
15068
+ try {
15069
+ const files = await readdir(reportsDir);
15070
+ const reports = files.filter((f) => f.startsWith("security-report-") && (f.endsWith(".md") || f.endsWith(".json"))).sort().reverse();
15071
+ if (!reportId) {
15072
+ if (reports.length === 0) {
15073
+ return { message: "\u{1F4ED} No security reports found. Run `/security scan` first." };
15074
+ }
15075
+ const list = reports.map((r, i) => {
15076
+ const date = r.replace("security-report-", "").replace(/\.(md|json)$/, "");
15077
+ return ` ${i + 1}. ${date}`;
15078
+ }).join("\n");
15079
+ return { message: `# Available Security Reports
15080
+
15081
+ ${list}
15082
+
15083
+ Use \`/security report <number>\` to view a specific report.` };
15084
+ }
15085
+ const index = parseInt(reportId, 10) - 1;
15086
+ if (!isNaN(index) && reports[index]) {
15087
+ const { readFile: readFile27 } = await import('fs/promises');
15088
+ const content = await readFile27(join(reportsDir, reports[index]), "utf-8");
15089
+ return { message: `# Security Report
15090
+
15091
+ ${content}` };
15092
+ }
15093
+ const match = reports.find((r) => r.includes(reportId));
15094
+ if (match) {
15095
+ const { readFile: readFile27 } = await import('fs/promises');
15096
+ const content = await readFile27(join(reportsDir, match), "utf-8");
15097
+ return { message: `# Security Report
15098
+
15099
+ ${content}` };
15100
+ }
15101
+ return { message: `\u274C Report "${reportId}" not found. Use \`/security report\` to see available reports.` };
15102
+ } catch (error) {
15103
+ return { message: "\u{1F4ED} No security reports found. Run `/security scan` first." };
15104
+ }
15105
+ }
15106
+ function parseArgs(args) {
15107
+ const result = {};
15108
+ const parts = args.split(/\s+/);
15109
+ for (let i = 0; i < parts.length; i++) {
15110
+ const part = parts[i];
15111
+ if (!part || !part.startsWith("--")) continue;
15112
+ const key = part.slice(2);
15113
+ const next = parts[i + 1];
15114
+ if (next && !next.startsWith("--")) {
15115
+ result[key] = next;
15116
+ i++;
15117
+ } else {
15118
+ result[key] = "true";
15119
+ }
15120
+ }
15121
+ return result;
15122
+ }
15123
+ function getHelpMessage() {
15124
+ return `
15125
+ # /security \u2014 Security Scanner
15126
+
15127
+ **Available Commands:**
15128
+
15129
+ 1. **/security scan** \u2014 Run full security scan
15130
+ Options: --depth quick|standard|deep, --format markdown|json|html
15131
+
15132
+ 2. **/security audit** \u2014 Run dependency audit + security scan
15133
+
15134
+ 3. **/security report** \u2014 List or view security reports
15135
+
15136
+ **Examples:**
15137
+ \`\`\`
15138
+ /security scan
15139
+ /security scan --depth deep --format html
15140
+ /security audit
15141
+ /security report
15142
+ \`\`\`
15143
+ `;
15144
+ }
15145
+ var securitySlashCommand = createSecuritySlashCommand();
13164
15146
 
13165
15147
  // src/extension/registry.ts
13166
15148
  var ExtensionRegistry = class {
@@ -14619,8 +16601,8 @@ ${mem}`);
14619
16601
  }
14620
16602
  async dirExists(p) {
14621
16603
  try {
14622
- const stat6 = await fsp2.stat(p);
14623
- return stat6.isDirectory();
16604
+ const stat8 = await fsp2.stat(p);
16605
+ return stat8.isDirectory();
14624
16606
  } catch {
14625
16607
  return false;
14626
16608
  }
@@ -15371,6 +17353,6 @@ function wrapApiForCapabilityCheck(plugin, api, log, enforce = false) {
15371
17353
  });
15372
17354
  }
15373
17355
 
15374
- export { AISpecBuilder, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, Agent, AgentError, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorBudgetError, DirectorStateCheckpoint, DoneConditionChecker, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, SelectiveCompactor, SessionAnalyzer, SessionError, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, ToolError, ToolExecutor, ToolRegistry, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, asBlocks, asText, atomicWrite, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildChildEnv, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, color, compileGlob, compileUserRegex, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDefaultPipelines, createDelegateTool, createMessage, createToolOutputSerializer, decryptConfigSecrets, detectNewlineStyle, downloadGitHubTarball, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatPlan, formatTodosList, getContextWindowMode, getTemplate, githubServer, googleMapsServer, isAgentError, isConfigError, isContextWindowModeId, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listTemplates, loadDirectorState, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeDirectorSessionFactory, matchAny, matchGlob, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseSkillRef, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveContextWindowPolicy, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, savePlan, saveTodosCheckpoint, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
17356
+ export { AISpecBuilder, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, Agent, AgentError, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorBudgetError, DirectorStateCheckpoint, DoneConditionChecker, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetUsageAggregator, GitignoreUpdater, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, ScopedEventBus, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolError, ToolExecutor, ToolRegistry, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, applyRosterBudget, asBlocks, asText, atomicWrite, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildChildEnv, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, color, compileGlob, compileUserRegex, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, context7Server, contextManagerTool, createAutoExecutor, createContextManagerTool, createDefaultPipelines, createDelegateTool, createMessage, createSecuritySlashCommand, createToolOutputSerializer, decryptConfigSecrets, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, detectNewlineStyle, downloadGitHubTarball, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatPlan, formatTodosList, getContextWindowMode, getTemplate, githubServer, googleMapsServer, isAgentError, isConfigError, isContextWindowModeId, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listTemplates, loadDirectorState, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeDirectorSessionFactory, matchAny, matchGlob, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseSkillRef, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveContextWindowPolicy, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, savePlan, saveTodosCheckpoint, securitySlashCommand, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
15375
17357
  //# sourceMappingURL=index.js.map
15376
17358
  //# sourceMappingURL=index.js.map