pwnkit-cli 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +53 -53
  2. package/dist/index.js +241 -189
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,34 +1,34 @@
1
1
  <p align="center">
2
- <img src="assets/pwnkit-icon.gif" alt="pwnkit" width="80" />
2
+ <img src="assets/pwnkit-icon.gif" alt="pwnkit" width="80" />
3
3
  </p>
4
4
 
5
5
  <h1 align="center">pwnkit</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>General-purpose autonomous pentesting framework</strong><br/>
9
- <em>Scan LLM endpoints. Audit npm packages. Review source code. Pentest web apps. Re-exploit to kill false positives.</em>
8
+ <strong>General-purpose autonomous pentesting framework</strong><br/>
9
+ <em>Scan LLM endpoints. Audit npm packages. Review source code. Pentest web apps. Re-exploit to kill false positives.</em>
10
10
  </p>
11
11
 
12
12
  <p align="center">
13
- <a href="https://www.npmjs.com/package/pwnkit-cli"><img src="https://img.shields.io/npm/v/pwnkit-cli?color=crimson&style=flat-square" alt="npm version" /></a>
14
- <a href="https://github.com/peaktwilight/pwnkit/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square" alt="license" /></a>
15
- <a href="https://github.com/peaktwilight/pwnkit/actions"><img src="https://img.shields.io/github/actions/workflow/status/peaktwilight/pwnkit/ci.yml?style=flat-square" alt="CI" /></a>
16
- <a href="https://github.com/peaktwilight/pwnkit/stargazers"><img src="https://img.shields.io/github/stars/peaktwilight/pwnkit?style=flat-square&color=gold" alt="stars" /></a>
17
- <a href="https://pwnkit.com"><img src="https://pwnkit.com/badge/peaktwilight/pwnkit" alt="pwnkit verified" /></a>
13
+ <a href="https://www.npmjs.com/package/pwnkit-cli"><img src="https://img.shields.io/npm/v/pwnkit-cli?color=crimson&style=flat-square" alt="npm version" /></a>
14
+ <a href="https://github.com/peaktwilight/pwnkit/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square" alt="license" /></a>
15
+ <a href="https://github.com/peaktwilight/pwnkit/actions"><img src="https://img.shields.io/github/actions/workflow/status/peaktwilight/pwnkit/ci.yml?style=flat-square" alt="CI" /></a>
16
+ <a href="https://github.com/peaktwilight/pwnkit/stargazers"><img src="https://img.shields.io/github/stars/peaktwilight/pwnkit?style=flat-square&color=gold" alt="stars" /></a>
17
+ <a href="https://pwnkit.com"><img src="https://pwnkit.com/badge/peaktwilight/pwnkit" alt="pwnkit verified" /></a>
18
18
  </p>
19
19
 
20
20
  <p align="center">
21
- <img src="assets/demo.gif" alt="pwnkit Demo" width="700" />
21
+ <img src="assets/demo.gif" alt="pwnkit Demo" width="700" />
22
22
  </p>
23
23
 
24
24
  <p align="center">
25
- <a href="#quick-start">Quick Start</a> &middot;
26
- <a href="#commands">Commands</a> &middot;
27
- <a href="#how-it-works">How It Works</a> &middot;
28
- <a href="#what-pwnkit-scans">What It Scans</a> &middot;
29
- <a href="#how-it-compares">Comparison</a> &middot;
30
- <a href="#github-action">CI/CD</a> &middot;
31
- <a href="#built-by">About</a>
25
+ <a href="#quick-start">Quick Start</a> &middot;
26
+ <a href="#commands">Commands</a> &middot;
27
+ <a href="#how-it-works">How It Works</a> &middot;
28
+ <a href="#what-pwnkit-scans">What It Scans</a> &middot;
29
+ <a href="#how-it-compares">Comparison</a> &middot;
30
+ <a href="#github-action">CI/CD</a> &middot;
31
+ <a href="#built-by">About</a>
32
32
  </p>
33
33
 
34
34
  ---
@@ -50,10 +50,10 @@ npx pwnkit-cli audit lodash
50
50
  npx pwnkit-cli review ./my-ai-app
51
51
 
52
52
  # Or just point pwnkit at a target — it auto-detects what to do
53
- npx pwnkit-cli express # audits npm package
54
- npx pwnkit-cli ./my-repo # reviews source code
55
- npx pwnkit-cli https://github.com/user/repo # clones and reviews
56
- npx pwnkit-cli https://example.com # scans web endpoint
53
+ npx pwnkit-cli express # audits npm package
54
+ npx pwnkit-cli ./my-repo # reviews source code
55
+ npx pwnkit-cli https://github.com/user/repo # clones and reviews
56
+ npx pwnkit-cli https://example.com # scans web endpoint
57
57
  ```
58
58
 
59
59
  That's it. pwnkit discovers your attack surface, launches targeted attacks, verifies findings, and generates a report — all in under 5 minutes.
@@ -90,16 +90,16 @@ pwnkit ships five commands — from quick API probes to deep source-level audits
90
90
  pwnkit runs autonomous AI agents in a research-then-verify pipeline. Each agent uses tools (`read_file`, `run_command`, `send_prompt`, `save_finding`) and makes multi-turn decisions — adapting its strategy based on what it learns:
91
91
 
92
92
  ```
93
- +-----------+ +------------------+ +-----------+
94
- | RESEARCH | --> | BLIND VERIFY | --> | REPORT |
95
- | (Discover | | (PoC + path only | | (Output) |
96
- | + Attack | | no reasoning) | | |
97
- | + PoC) | +------------------+ +-----------+
98
- +-----------+ Runs in parallel Only confirmed
99
- Single agent per finding — findings in SARIF,
100
- session: recon, independently Markdown, and JSON
101
- payloads, and reproduces or with severity +
102
- proof-of-concept kills finding remediation
93
+ +-----------+ +------------------+ +-----------+
94
+ | RESEARCH | --> | BLIND VERIFY | --> | REPORT |
95
+ | (Discover | | (PoC + path only | | (Output) |
96
+ | + Attack | | no reasoning) | | |
97
+ | + PoC) | +------------------+ +-----------+
98
+ +-----------+ Runs in parallel Only confirmed
99
+ Single agent per finding — findings in SARIF,
100
+ session: recon, independently Markdown, and JSON
101
+ payloads, and reproduces or with severity +
102
+ proof-of-concept kills finding remediation
103
103
  ```
104
104
 
105
105
  | Agent | Role | What It Does |
@@ -178,7 +178,7 @@ Bring your own agent CLI — pwnkit orchestrates it:
178
178
  | `claude` | `--runtime claude` | Attack generation, deep analysis — spawns Claude Code CLI |
179
179
  | `codex` | `--runtime codex` | Verification, source analysis — spawns Codex CLI |
180
180
  | `gemini` | `--runtime gemini` | Large context source analysis — spawns Gemini CLI |
181
- | `opencode` | `--runtime opencode` | Multi-provider flexibility — spawns OpenCode CLI |
181
+ | `` | `--runtime ` | Multi-provider flexibility — spawns CLI |
182
182
  | `auto` | `--runtime auto` | Best overall — auto-detects installed runtimes, picks best per stage |
183
183
 
184
184
  Combined with scan modes:
@@ -190,7 +190,7 @@ Combined with scan modes:
190
190
  | `mcp` | `--mode mcp` | Connect to MCP server, enumerate tools, test each for security issues |
191
191
  | `web` | `--mode web` | Full web pentesting — SQLi, XSS, SSRF, auth bypass, IDOR |
192
192
 
193
- > `deep`, `mcp`, and `web` modes require a process runtime (`claude`, `codex`, `gemini`, `opencode`, or `auto`).
193
+ > `deep`, `mcp`, and `web` modes require a process runtime (`claude`, `codex`, `gemini`, ``, or `auto`).
194
194
 
195
195
  ## How It Compares
196
196
 
@@ -220,28 +220,28 @@ name: AI Security Scan
220
220
  on: [push, pull_request]
221
221
 
222
222
  permissions:
223
- contents: read
224
- security-events: write
223
+ contents: read
224
+ security-events: write
225
225
 
226
226
  jobs:
227
- pwnkit:
228
- runs-on: ubuntu-latest
229
- steps:
230
- - uses: actions/checkout@v4
231
-
232
- - name: Run pwnkit
233
- uses: peaktwilight/pwnkit/action@v1
234
- with:
235
- target: ${{ secrets.STAGING_API_URL }}
236
- depth: default # quick | default | deep
237
- fail-on-severity: high # critical | high | medium | low | info | none
238
- env:
239
- OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
240
-
241
- - name: Upload SARIF
242
- uses: github/codeql-action/upload-sarif@v3
243
- with:
244
- sarif_file: pwnkit-report/report.sarif
227
+ pwnkit:
228
+ runs-on: ubuntu-latest
229
+ steps:
230
+ - uses: actions/checkout@v4
231
+
232
+ - name: Run pwnkit
233
+ uses: peaktwilight/pwnkit/action@v1
234
+ with:
235
+ target: ${{ secrets.STAGING_API_URL }}
236
+ depth: default # quick | default | deep
237
+ fail-on-severity: high # critical | high | medium | low | info | none
238
+ env:
239
+ OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
240
+
241
+ - name: Upload SARIF
242
+ uses: github/codeql-action/upload-sarif@v3
243
+ with:
244
+ sarif_file: pwnkit-report/report.sarif
245
245
  ```
246
246
 
247
247
  > **API Key Priority:** pwnkit checks for `OPENROUTER_API_KEY` first, then `ANTHROPIC_API_KEY`, then `OPENAI_API_KEY`. OpenRouter gives you access to many models (including free ones) through a single key at [openrouter.ai](https://openrouter.ai).
@@ -291,7 +291,7 @@ Finding lifecycle: `discovered → verified → confirmed → scored → reporte
291
291
  - [x] MCP server scanning
292
292
  - [x] npm package auditing
293
293
  - [x] Source code review (local + GitHub)
294
- - [x] Multi-runtime support (Claude, Codex, Gemini, OpenCode)
294
+ - [x] Multi-runtime support (Claude, Codex, Gemini)
295
295
  - [x] Multi-turn agentic attacks (agents adapt payloads based on responses)
296
296
  - [x] Web pentesting mode (SQLi, XSS, SSRF, auth bypass, IDOR)
297
297
  - [ ] RAG pipeline security (poisoning, extraction)
package/dist/index.js CHANGED
@@ -3561,7 +3561,7 @@ var VERSION, DEPTH_CONFIG;
3561
3561
  var init_constants = __esm({
3562
3562
  "packages/shared/dist/constants.js"() {
3563
3563
  "use strict";
3564
- VERSION = "0.3.0";
3564
+ VERSION = "0.3.1";
3565
3565
  DEPTH_CONFIG = {
3566
3566
  quick: { maxTemplates: 5, maxPayloadsPerTemplate: 1, multiTurn: false },
3567
3567
  default: { maxTemplates: 20, maxPayloadsPerTemplate: 3, multiTurn: false },
@@ -10924,8 +10924,7 @@ var init_process = __esm({
10924
10924
  RUNTIME_COMMANDS = {
10925
10925
  claude: "claude",
10926
10926
  codex: "codex",
10927
- gemini: "gemini",
10928
- opencode: "opencode"
10927
+ gemini: "gemini"
10929
10928
  };
10930
10929
  ProcessRuntime = class {
10931
10930
  type;
@@ -11064,8 +11063,6 @@ var init_process = __esm({
11064
11063
  const args = ["-p", prompt, "--output-format", "stream-json"];
11065
11064
  return args;
11066
11065
  }
11067
- case "opencode":
11068
- return ["-p", prompt, "--output", "text"];
11069
11066
  default:
11070
11067
  return ["-p", prompt];
11071
11068
  }
@@ -11089,6 +11086,75 @@ var init_process = __esm({
11089
11086
  }
11090
11087
  });
11091
11088
 
11089
+ // packages/core/dist/runtime/registry.js
11090
+ var registry_exports = {};
11091
+ __export(registry_exports, {
11092
+ RUNTIME_REGISTRY: () => RUNTIME_REGISTRY,
11093
+ detectAvailableRuntimes: () => detectAvailableRuntimes,
11094
+ getRuntimeInfo: () => getRuntimeInfo,
11095
+ pickRuntimeForStage: () => pickRuntimeForStage
11096
+ });
11097
+ function pickRuntimeForStage(stage, availableRuntimes) {
11098
+ const prefs = STAGE_PREFERENCES[stage];
11099
+ for (const rt2 of prefs) {
11100
+ if (availableRuntimes.has(rt2))
11101
+ return rt2;
11102
+ }
11103
+ const first = availableRuntimes.values().next();
11104
+ return first.done ? "claude" : first.value;
11105
+ }
11106
+ async function detectAvailableRuntimes() {
11107
+ const { ProcessRuntime: ProcessRuntime2 } = await Promise.resolve().then(() => (init_process(), process_exports));
11108
+ const available = /* @__PURE__ */ new Set();
11109
+ const checks = RUNTIME_REGISTRY.map(async (info) => {
11110
+ const rt2 = new ProcessRuntime2({ type: info.type, timeout: 5e3 });
11111
+ if (await rt2.isAvailable()) {
11112
+ available.add(info.type);
11113
+ }
11114
+ });
11115
+ await Promise.all(checks);
11116
+ return available;
11117
+ }
11118
+ function getRuntimeInfo(type) {
11119
+ return RUNTIME_REGISTRY.find((r) => r.type === type);
11120
+ }
11121
+ var RUNTIME_REGISTRY, STAGE_PREFERENCES;
11122
+ var init_registry = __esm({
11123
+ "packages/core/dist/runtime/registry.js"() {
11124
+ "use strict";
11125
+ RUNTIME_REGISTRY = [
11126
+ {
11127
+ type: "claude",
11128
+ command: "claude",
11129
+ description: "Claude Code CLI \u2014 best for creative attack generation and deep analysis",
11130
+ strengths: ["attack", "source-analysis", "report"],
11131
+ supportsSystemPrompt: true
11132
+ },
11133
+ {
11134
+ type: "codex",
11135
+ command: "codex",
11136
+ description: "Codex CLI \u2014 strong at code review, pattern matching, and verification",
11137
+ strengths: ["verify", "source-analysis", "discovery"],
11138
+ supportsSystemPrompt: false
11139
+ },
11140
+ {
11141
+ type: "gemini",
11142
+ command: "gemini",
11143
+ description: "Gemini CLI \u2014 large context window, good for source analysis",
11144
+ strengths: ["source-analysis", "report", "discovery"],
11145
+ supportsSystemPrompt: false
11146
+ }
11147
+ ];
11148
+ STAGE_PREFERENCES = {
11149
+ "discovery": ["claude", "codex", "gemini"],
11150
+ "source-analysis": ["claude", "gemini", "codex"],
11151
+ "attack": ["claude", "codex", "gemini"],
11152
+ "verify": ["codex", "claude", "gemini"],
11153
+ "report": ["claude", "gemini", "codex"]
11154
+ };
11155
+ }
11156
+ });
11157
+
11092
11158
  // packages/db/dist/schema.js
11093
11159
  var schema_exports = {};
11094
11160
  __export(schema_exports, {
@@ -44109,14 +44175,22 @@ var init_devtools_window_polyfill = __esm({
44109
44175
  }
44110
44176
  });
44111
44177
 
44178
+ // stub:react-devtools-core
44179
+ var react_devtools_core_default;
44180
+ var init_react_devtools_core = __esm({
44181
+ "stub:react-devtools-core"() {
44182
+ react_devtools_core_default = {};
44183
+ }
44184
+ });
44185
+
44112
44186
  // node_modules/.pnpm/ink@6.8.0_@types+react@19.2.14_react@19.2.4/node_modules/ink/build/devtools.js
44113
44187
  var devtools_exports = {};
44114
- import devtools from "react-devtools-core";
44115
44188
  var init_devtools = __esm({
44116
44189
  "node_modules/.pnpm/ink@6.8.0_@types+react@19.2.14_react@19.2.4/node_modules/ink/build/devtools.js"() {
44117
44190
  init_devtools_window_polyfill();
44118
- devtools.initialize();
44119
- devtools.connectToDevTools();
44191
+ init_react_devtools_core();
44192
+ react_devtools_core_default.initialize();
44193
+ react_devtools_core_default.connectToDevTools();
44120
44194
  }
44121
44195
  });
44122
44196
 
@@ -52419,12 +52493,20 @@ function formatDuration2(ms) {
52419
52493
  }
52420
52494
  function StageRow({ stage }) {
52421
52495
  const icon = stage.status === "done" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: GREEN, children: "\u2713" }) : stage.status === "running" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: CRIMSON, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(build_default, { type: "dots" }) }) : stage.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: CRIMSON, children: "\u2717" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: GRAY, children: "\u25CC" });
52496
+ let verifyCount = "";
52497
+ if (stage.id === "verify" && stage.status === "done" && stage.actions.length > 0) {
52498
+ const confirmed = stage.actions.filter((a) => a.startsWith("\u2713")).length;
52499
+ const total = stage.actions.filter((a) => a.startsWith("\u2713") || a.startsWith("\u2717")).length;
52500
+ if (total > 0) {
52501
+ verifyCount = `${confirmed}/${total} confirmed`;
52502
+ }
52503
+ }
52422
52504
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { flexDirection: "column", children: [
52423
52505
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { gap: 1, children: [
52424
52506
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: " " }),
52425
52507
  icon,
52426
52508
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { bold: true, color: stage.status === "pending" ? GRAY : void 0, children: stage.label.padEnd(12) }),
52427
- stage.detail && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: stage.status === "done" ? GRAY : void 0, dimColor: stage.status === "done", children: stage.detail }),
52509
+ verifyCount ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: GREEN, children: verifyCount }) : stage.detail ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: stage.status === "done" ? GRAY : void 0, dimColor: stage.status === "done", children: stage.detail }) : null,
52428
52510
  stage.duration !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { color: GRAY, children: [
52429
52511
  " ",
52430
52512
  formatDuration2(stage.duration)
@@ -52434,7 +52516,19 @@ function StageRow({ stage }) {
52434
52516
  if (stage.id === "verify") {
52435
52517
  const isConfirmed = action.startsWith("\u2713");
52436
52518
  const isRejected = action.startsWith("\u2717");
52437
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { color: isConfirmed ? GREEN : isRejected ? GRAY : CYAN, children: [
52519
+ if (isConfirmed) {
52520
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { color: GREEN, bold: true, children: [
52521
+ "\u2192 ",
52522
+ action
52523
+ ] }, i);
52524
+ }
52525
+ if (isRejected) {
52526
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { color: CRIMSON, dimColor: true, strikethrough: true, children: [
52527
+ "\u2192 ",
52528
+ action
52529
+ ] }, i);
52530
+ }
52531
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { color: CYAN, children: [
52438
52532
  "\u2192 ",
52439
52533
  action
52440
52534
  ] }, i);
@@ -52627,10 +52721,12 @@ function renderScanUI(opts) {
52627
52721
  if (event.type === "verify:result") {
52628
52722
  const data = event.data;
52629
52723
  const confirmed = data?.confirmed;
52630
- const msg2 = event.message ?? "";
52724
+ const title = data?.title ?? event.message;
52725
+ const reason = data?.reason;
52726
+ const label = confirmed ? `\u2713 ${title}` : `\u2717 ${title}${reason ? ` \u2014 ${reason}` : ""}`;
52631
52727
  updateStage("verify", (s) => ({
52632
52728
  ...s,
52633
- actions: [...s.actions, `${confirmed ? "\u2713" : "\u2717"} ${msg2}`]
52729
+ actions: [...s.actions, label]
52634
52730
  }));
52635
52731
  return;
52636
52732
  }
@@ -54859,68 +54955,7 @@ var LlmApiRuntime = class {
54859
54955
 
54860
54956
  // packages/core/dist/runtime/index.js
54861
54957
  init_process();
54862
-
54863
- // packages/core/dist/runtime/registry.js
54864
- var RUNTIME_REGISTRY = [
54865
- {
54866
- type: "claude",
54867
- command: "claude",
54868
- description: "Claude Code CLI \u2014 best for creative attack generation and deep analysis",
54869
- strengths: ["attack", "source-analysis", "report"],
54870
- supportsSystemPrompt: true
54871
- },
54872
- {
54873
- type: "codex",
54874
- command: "codex",
54875
- description: "Codex CLI \u2014 strong at code review, pattern matching, and verification",
54876
- strengths: ["verify", "source-analysis", "discovery"],
54877
- supportsSystemPrompt: false
54878
- },
54879
- {
54880
- type: "gemini",
54881
- command: "gemini",
54882
- description: "Gemini CLI \u2014 large context window, good for source analysis",
54883
- strengths: ["source-analysis", "report", "discovery"],
54884
- supportsSystemPrompt: false
54885
- },
54886
- {
54887
- type: "opencode",
54888
- command: "opencode",
54889
- description: "OpenCode CLI \u2014 multi-provider runtime with flexible model selection",
54890
- strengths: ["source-analysis", "verify", "attack"],
54891
- supportsSystemPrompt: false
54892
- }
54893
- ];
54894
- var STAGE_PREFERENCES = {
54895
- "discovery": ["claude", "codex", "gemini", "opencode"],
54896
- "source-analysis": ["claude", "gemini", "codex", "opencode"],
54897
- "attack": ["claude", "codex", "gemini", "opencode"],
54898
- "verify": ["codex", "claude", "gemini", "opencode"],
54899
- "report": ["claude", "gemini", "codex", "opencode"]
54900
- };
54901
- function pickRuntimeForStage(stage, availableRuntimes) {
54902
- const prefs = STAGE_PREFERENCES[stage];
54903
- for (const rt2 of prefs) {
54904
- if (availableRuntimes.has(rt2))
54905
- return rt2;
54906
- }
54907
- const first = availableRuntimes.values().next();
54908
- return first.done ? "claude" : first.value;
54909
- }
54910
- async function detectAvailableRuntimes() {
54911
- const { ProcessRuntime: ProcessRuntime2 } = await Promise.resolve().then(() => (init_process(), process_exports));
54912
- const available = /* @__PURE__ */ new Set();
54913
- const checks = RUNTIME_REGISTRY.map(async (info) => {
54914
- const rt2 = new ProcessRuntime2({ type: info.type, timeout: 5e3 });
54915
- if (await rt2.isAvailable()) {
54916
- available.add(info.type);
54917
- }
54918
- });
54919
- await Promise.all(checks);
54920
- return available;
54921
- }
54922
-
54923
- // packages/core/dist/runtime/index.js
54958
+ init_registry();
54924
54959
  init_process();
54925
54960
  function createRuntime(config) {
54926
54961
  switch (config.type) {
@@ -54929,11 +54964,13 @@ function createRuntime(config) {
54929
54964
  case "claude":
54930
54965
  case "codex":
54931
54966
  case "gemini":
54932
- case "opencode":
54933
54967
  return new ProcessRuntime(config);
54934
54968
  }
54935
54969
  }
54936
54970
 
54971
+ // packages/core/dist/scanner.js
54972
+ init_registry();
54973
+
54937
54974
  // packages/core/dist/http.js
54938
54975
  async function sendPrompt(target, prompt, options) {
54939
54976
  const start = Date.now();
@@ -56706,7 +56743,7 @@ async function scan(config, onEvent, dbPath) {
56706
56743
  if (isAuto) {
56707
56744
  availableRuntimes = await detectAvailableRuntimes();
56708
56745
  if (availableRuntimes.size === 0) {
56709
- throw new Error("--runtime auto: no CLI runtimes (claude, codex, gemini, opencode) detected. Install at least one or use --runtime api.");
56746
+ throw new Error("--runtime auto: no CLI runtimes (claude, codex, gemini) detected. Install at least one or use --runtime api.");
56710
56747
  }
56711
56748
  }
56712
56749
  function getRuntimeForStage(stage) {
@@ -57559,9 +57596,12 @@ Rate based on REAL exploitability:
57559
57596
  - Call done when you've thoroughly reviewed the codebase`;
57560
57597
  }
57561
57598
 
57599
+ // packages/core/dist/agent-runner.js
57600
+ init_registry();
57601
+
57562
57602
  // packages/core/dist/shared-analysis.js
57563
57603
  import { execFileSync } from "node:child_process";
57564
- var CLI_RUNTIME_TYPES = /* @__PURE__ */ new Set(["claude", "codex", "gemini", "opencode"]);
57604
+ var CLI_RUNTIME_TYPES = /* @__PURE__ */ new Set(["claude", "codex", "gemini"]);
57565
57605
  function bufferToString(value) {
57566
57606
  if (!value) {
57567
57607
  return "";
@@ -58700,77 +58740,87 @@ async function runPipeline(opts) {
58700
58740
  stage: "analyze",
58701
58741
  message: `Analysis complete: ${semgrepFindings.length} semgrep findings, ${npmAuditFindings.length} npm advisories`
58702
58742
  });
58703
- emit({ type: "stage:start", stage: "research", message: "Researching vulnerabilities..." });
58704
- const researchEmit = (event) => {
58705
- if (event.type === "stage:start") {
58706
- emit({ type: "stage:start", stage: "research", message: event.message });
58707
- } else if (event.type === "finding") {
58708
- emit(event);
58709
- }
58710
- };
58743
+ const hasApiKey = !!(opts.apiKey || process.env.OPENROUTER_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
58744
+ const { detectAvailableRuntimes: detectAvailableRuntimes2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
58745
+ const availableRuntimes = await detectAvailableRuntimes2();
58746
+ const hasCliRuntime = availableRuntimes.size > 0;
58747
+ if (!hasApiKey && !hasCliRuntime) {
58748
+ warnings.push({ stage: "research", message: "No API key or CLI runtime available. AI analysis skipped. Set OPENROUTER_API_KEY, ANTHROPIC_API_KEY, or OPENAI_API_KEY." });
58749
+ emit({ type: "stage:end", stage: "research", message: "Skipped \u2014 no API key or CLI runtime" });
58750
+ emit({ type: "stage:end", stage: "verify", message: "Skipped" });
58751
+ }
58711
58752
  let findings2 = [];
58712
- if (prepared.resolvedType === "npm-package" || prepared.resolvedType === "source-code") {
58713
- const targetLabel = prepared.resolvedType === "npm-package" ? `npm package ${prepared.packageName}@${prepared.packageVersion}` : "repository";
58714
- const agentSystemPrompt = researchPrompt(prepared.scopePath, semgrepFindings.map((f) => ({ ruleId: f.ruleId, message: f.message, path: f.path, startLine: f.startLine })), npmAuditFindings.map((f) => ({ name: f.name, severity: f.severity, title: f.title })), targetLabel);
58715
- findings2 = await runAnalysisAgent({
58716
- role: prepared.resolvedType === "npm-package" ? "audit" : "review",
58717
- scopePath: prepared.scopePath,
58718
- target: prepared.resolvedTarget,
58719
- scanId,
58720
- config: {
58721
- runtime: opts.runtime,
58722
- timeout: opts.timeout,
58723
- depth: opts.depth,
58724
- apiKey: opts.apiKey,
58725
- model: opts.model
58726
- },
58727
- db,
58728
- emit: researchEmit,
58729
- cliPrompt: buildCliPrompt(prepared.scopePath, semgrepFindings, npmAuditFindings, targetLabel),
58730
- agentSystemPrompt,
58731
- cliSystemPrompt: "You are a security researcher performing an authorized source code audit. For EACH vulnerability you find, output it using the exact ---FINDING--- / ---END--- format specified in the prompt. Do NOT write prose analysis \u2014 only output structured finding blocks. If you find no vulnerabilities, say 'No vulnerabilities found.' and nothing else."
58732
- });
58733
- } else {
58734
- warnings.push({
58753
+ if (hasApiKey || hasCliRuntime) {
58754
+ emit({ type: "stage:start", stage: "research", message: "Researching vulnerabilities..." });
58755
+ const researchEmit = (event) => {
58756
+ if (event.type === "stage:start") {
58757
+ emit({ type: "stage:start", stage: "research", message: event.message });
58758
+ } else if (event.type === "finding") {
58759
+ emit(event);
58760
+ }
58761
+ };
58762
+ if (prepared.resolvedType === "npm-package" || prepared.resolvedType === "source-code") {
58763
+ const targetLabel = prepared.resolvedType === "npm-package" ? `npm package ${prepared.packageName}@${prepared.packageVersion}` : "repository";
58764
+ const agentSystemPrompt = researchPrompt(prepared.scopePath, semgrepFindings.map((f) => ({ ruleId: f.ruleId, message: f.message, path: f.path, startLine: f.startLine })), npmAuditFindings.map((f) => ({ name: f.name, severity: f.severity, title: f.title })), targetLabel);
58765
+ findings2 = await runAnalysisAgent({
58766
+ role: prepared.resolvedType === "npm-package" ? "audit" : "review",
58767
+ scopePath: prepared.scopePath,
58768
+ target: prepared.resolvedTarget,
58769
+ scanId,
58770
+ config: {
58771
+ runtime: opts.runtime,
58772
+ timeout: opts.timeout,
58773
+ depth: opts.depth,
58774
+ apiKey: opts.apiKey,
58775
+ model: opts.model
58776
+ },
58777
+ db,
58778
+ emit: researchEmit,
58779
+ cliPrompt: buildCliPrompt(prepared.scopePath, semgrepFindings, npmAuditFindings, targetLabel),
58780
+ agentSystemPrompt,
58781
+ cliSystemPrompt: "You are a security researcher performing an authorized source code audit. For EACH vulnerability you find, output it using the exact ---FINDING--- / ---END--- format specified in the prompt. Do NOT write prose analysis \u2014 only output structured finding blocks. If you find no vulnerabilities, say 'No vulnerabilities found.' and nothing else."
58782
+ });
58783
+ } else {
58784
+ warnings.push({
58785
+ stage: "research",
58786
+ message: `Target type "${prepared.resolvedType}" is not yet supported in the unified pipeline. Use 'pwnkit scan' for URL/web-app targets.`
58787
+ });
58788
+ }
58789
+ emit({
58790
+ type: "stage:end",
58735
58791
  stage: "research",
58736
- message: `Target type "${prepared.resolvedType}" is not yet supported in the unified pipeline. Use 'pwnkit scan' for URL/web-app targets.`
58792
+ message: `${findings2.length} findings discovered`
58737
58793
  });
58738
- }
58739
- emit({
58740
- type: "stage:end",
58741
- stage: "research",
58742
- message: `${findings2.length} findings discovered`
58743
- });
58744
- if (findings2.length > 0 && (prepared.resolvedType === "source-code" || prepared.resolvedType === "npm-package")) {
58745
- emit({ type: "stage:start", stage: "verify", message: `Blind-verifying ${findings2.length} findings...` });
58746
- try {
58747
- const verifyResults = await Promise.all(findings2.map(async (finding) => {
58748
- const filePath = finding.evidence.request || "";
58749
- const poc = finding.evidence.response || finding.evidence.analysis || "";
58750
- const claimedSeverity = finding.severity;
58751
- const verifySystemPrompt = blindVerifyPrompt(filePath, poc, claimedSeverity, prepared.scopePath);
58752
- const verifyEmit = (event) => {
58753
- if (event.type === "finding") {
58754
- emit({ type: "verify:result", message: `Confirmed: ${finding.title}`, data: { confirmed: true, finding } });
58755
- }
58756
- };
58757
- try {
58758
- const verifiedFindings = await runAnalysisAgent({
58759
- role: "review",
58760
- scopePath: prepared.scopePath,
58761
- target: prepared.resolvedTarget,
58762
- scanId,
58763
- config: {
58764
- runtime: "api",
58765
- // API runtime: cheaper and faster for focused verification
58766
- timeout: Math.min(opts.timeout ?? 12e4, 12e4),
58767
- depth: "quick",
58768
- apiKey: opts.apiKey,
58769
- model: opts.model
58770
- },
58771
- db,
58772
- emit: verifyEmit,
58773
- cliPrompt: `Verify this vulnerability in ${filePath}:
58794
+ if (findings2.length > 0 && (prepared.resolvedType === "source-code" || prepared.resolvedType === "npm-package")) {
58795
+ emit({ type: "stage:start", stage: "verify", message: `Blind-verifying ${findings2.length} findings...` });
58796
+ try {
58797
+ const verifyResults = await Promise.all(findings2.map(async (finding) => {
58798
+ const filePath = finding.evidence.request || "";
58799
+ const poc = finding.evidence.response || finding.evidence.analysis || "";
58800
+ const claimedSeverity = finding.severity;
58801
+ const verifySystemPrompt = blindVerifyPrompt(filePath, poc, claimedSeverity, prepared.scopePath);
58802
+ const verifyEmit = (event) => {
58803
+ if (event.type === "finding") {
58804
+ emit({ type: "verify:result", message: `Confirmed: ${finding.title}`, data: { confirmed: true, finding } });
58805
+ }
58806
+ };
58807
+ try {
58808
+ const verifiedFindings = await runAnalysisAgent({
58809
+ role: "review",
58810
+ scopePath: prepared.scopePath,
58811
+ target: prepared.resolvedTarget,
58812
+ scanId,
58813
+ config: {
58814
+ runtime: "api",
58815
+ // API runtime: cheaper and faster for focused verification
58816
+ timeout: Math.min(opts.timeout ?? 12e4, 12e4),
58817
+ depth: "quick",
58818
+ apiKey: opts.apiKey,
58819
+ model: opts.model
58820
+ },
58821
+ db,
58822
+ emit: verifyEmit,
58823
+ cliPrompt: `Verify this vulnerability in ${filePath}:
58774
58824
 
58775
58825
  PoC:
58776
58826
  ${poc}
@@ -58778,44 +58828,46 @@ ${poc}
58778
58828
  Claimed severity: ${claimedSeverity}
58779
58829
 
58780
58830
  Read the file, trace data flow, confirm or reject.`,
58781
- agentSystemPrompt: verifySystemPrompt,
58782
- cliSystemPrompt: "You are a blind verification agent. Read the file, trace the PoC, confirm or reject the vulnerability."
58783
- });
58784
- const confirmed = verifiedFindings.length > 0;
58785
- return { finding, confirmed, verifiedFinding: verifiedFindings[0] ?? null };
58786
- } catch (err) {
58787
- const msg = err instanceof Error ? err.message : String(err);
58788
- warnings.push({ stage: "verify", message: `Verification failed for "${finding.title}": ${msg}` });
58789
- return { finding, confirmed: true, verifiedFinding: null };
58790
- }
58791
- }));
58792
- let confirmedCount = 0;
58793
- let rejectedCount = 0;
58794
- findings2 = verifyResults.map(({ finding, confirmed, verifiedFinding }) => {
58795
- if (confirmed) {
58796
- confirmedCount++;
58797
- emit({ type: "verify:result", message: `Confirmed: ${finding.title}`, data: { confirmed: true, title: finding.title } });
58798
- return {
58799
- ...finding,
58800
- status: "verified",
58801
- confidence: verifiedFinding?.confidence ?? finding.confidence,
58802
- severity: verifiedFinding?.severity ?? finding.severity
58803
- };
58804
- } else {
58805
- rejectedCount++;
58806
- emit({ type: "verify:result", message: `Rejected: ${finding.title}`, data: { confirmed: false, title: finding.title } });
58807
- return { ...finding, status: "false-positive" };
58808
- }
58809
- });
58810
- emit({
58811
- type: "stage:end",
58812
- stage: "verify",
58813
- message: `Verification complete: ${confirmedCount} confirmed, ${rejectedCount} rejected`
58814
- });
58815
- } catch (err) {
58816
- const msg = err instanceof Error ? err.message : String(err);
58817
- warnings.push({ stage: "verify", message: `Verification failed: ${msg}` });
58818
- emit({ type: "stage:end", stage: "verify", message: `Verification failed: ${msg}` });
58831
+ agentSystemPrompt: verifySystemPrompt,
58832
+ cliSystemPrompt: "You are a blind verification agent. Read the file, trace the PoC, confirm or reject the vulnerability."
58833
+ });
58834
+ const confirmed = verifiedFindings.length > 0;
58835
+ const rejectionReason = confirmed ? void 0 : "Could not independently reproduce";
58836
+ return { finding, confirmed, verifiedFinding: verifiedFindings[0] ?? null, rejectionReason };
58837
+ } catch (err) {
58838
+ const msg = err instanceof Error ? err.message : String(err);
58839
+ warnings.push({ stage: "verify", message: `Verification failed for "${finding.title}": ${msg}` });
58840
+ return { finding, confirmed: true, verifiedFinding: null };
58841
+ }
58842
+ }));
58843
+ let confirmedCount = 0;
58844
+ let rejectedCount = 0;
58845
+ findings2 = verifyResults.map(({ finding, confirmed, verifiedFinding, rejectionReason }) => {
58846
+ if (confirmed) {
58847
+ confirmedCount++;
58848
+ emit({ type: "verify:result", message: `Confirmed: ${finding.title}`, data: { confirmed: true, title: finding.title } });
58849
+ return {
58850
+ ...finding,
58851
+ status: "verified",
58852
+ confidence: verifiedFinding?.confidence ?? finding.confidence,
58853
+ severity: verifiedFinding?.severity ?? finding.severity
58854
+ };
58855
+ } else {
58856
+ rejectedCount++;
58857
+ emit({ type: "verify:result", message: `Rejected: ${finding.title}`, data: { confirmed: false, title: finding.title, reason: rejectionReason ?? "Could not independently reproduce" } });
58858
+ return { ...finding, status: "false-positive" };
58859
+ }
58860
+ });
58861
+ emit({
58862
+ type: "stage:end",
58863
+ stage: "verify",
58864
+ message: `Verification complete: ${confirmedCount} confirmed, ${rejectedCount} rejected`
58865
+ });
58866
+ } catch (err) {
58867
+ const msg = err instanceof Error ? err.message : String(err);
58868
+ warnings.push({ stage: "verify", message: `Verification failed: ${msg}` });
58869
+ emit({ type: "stage:end", stage: "verify", message: `Verification failed: ${msg}` });
58870
+ }
58819
58871
  }
58820
58872
  }
58821
58873
  const confirmedFindings = findings2.filter((f) => f.status !== "false-positive");
@@ -59577,7 +59629,7 @@ function createEventHandler(opts) {
59577
59629
  // packages/cli/src/commands/scan.ts
59578
59630
  init_utils();
59579
59631
  function registerScanCommand(program3) {
59580
- program3.command("scan").description("Run security scan against an LLM endpoint").requiredOption("--target <url>", "Target API endpoint URL").option("--depth <depth>", "Scan depth: quick, default, deep", "default").option("--format <format>", "Output format: terminal, json, md", "terminal").option("--runtime <runtime>", "Runtime: api, claude, codex, gemini, opencode, auto", "api").option("--mode <mode>", "Scan mode: probe, deep, mcp, web", "probe").option("--repo <path>", "Path to target repo for deep scan source analysis").option("--timeout <ms>", "Request timeout in milliseconds", "30000").option("--agentic", "Use multi-turn agentic scan with tool use and SQLite persistence", false).option("--db-path <path>", "Path to SQLite database (default: ~/.pwnkit/pwnkit.db)").option("--api-key <key>", "API key for LLM provider (or set OPENROUTER_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY)").option("--model <model>", "LLM model to use (or set PWNKIT_MODEL)").option("--verbose", "Show detailed output with live attack replay", false).option("--replay", "Replay the last scan's results as an animated attack chain", false).action(async (opts) => {
59632
+ program3.command("scan").description("Run security scan against an LLM endpoint").requiredOption("--target <url>", "Target API endpoint URL").option("--depth <depth>", "Scan depth: quick, default, deep", "default").option("--format <format>", "Output format: terminal, json, md", "terminal").option("--runtime <runtime>", "Runtime: api, claude, codex, gemini, auto", "api").option("--mode <mode>", "Scan mode: probe, deep, mcp, web", "probe").option("--repo <path>", "Path to target repo for deep scan source analysis").option("--timeout <ms>", "Request timeout in milliseconds", "30000").option("--agentic", "Use multi-turn agentic scan with tool use and SQLite persistence", false).option("--db-path <path>", "Path to SQLite database (default: ~/.pwnkit/pwnkit.db)").option("--api-key <key>", "API key for LLM provider (or set OPENROUTER_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY)").option("--model <model>", "LLM model to use (or set PWNKIT_MODEL)").option("--verbose", "Show detailed output with live attack replay", false).option("--replay", "Replay the last scan's results as an animated attack chain", false).action(async (opts) => {
59581
59633
  const depth = opts.depth;
59582
59634
  const format = opts.format === "md" ? "markdown" : opts.format;
59583
59635
  const runtime = opts.runtime;
@@ -59635,7 +59687,7 @@ function registerScanCommand(program3) {
59635
59687
  process.exit(2);
59636
59688
  }
59637
59689
  }
59638
- const validRuntimes = ["api", "claude", "codex", "gemini", "opencode", "auto"];
59690
+ const validRuntimes = ["api", "claude", "codex", "gemini", "auto"];
59639
59691
  if (!validRuntimes.includes(runtime)) {
59640
59692
  console.error(
59641
59693
  source_default.red(`Unknown runtime '${runtime}'. Valid: ${validRuntimes.join(", ")}`)
@@ -59644,7 +59696,7 @@ function registerScanCommand(program3) {
59644
59696
  }
59645
59697
  if (mode !== "probe" && mode !== "web" && runtime === "api") {
59646
59698
  console.error(
59647
- source_default.red(`Mode '${mode}' requires a process runtime (claude, codex, gemini, opencode, or auto)`)
59699
+ source_default.red(`Mode '${mode}' requires a process runtime (claude, codex, gemini, or auto)`)
59648
59700
  );
59649
59701
  process.exit(2);
59650
59702
  }
@@ -59957,7 +60009,7 @@ init_source();
59957
60009
  init_dist();
59958
60010
  init_utils();
59959
60011
  function registerReviewCommand(program3) {
59960
- program3.command("review").description("Deep source code security review of a repository").argument("<repo>", "Local path or git URL to review").option("--depth <depth>", "Review depth: quick, default, deep", "default").option("--format <format>", "Output format: terminal, json, md", "terminal").option("--runtime <runtime>", "Runtime: auto, claude, codex, gemini, opencode, api", "auto").option("--db-path <path>", "Path to SQLite database").option("--api-key <key>", "API key for LLM provider (or set OPENROUTER_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY)").option("--model <model>", "LLM model to use (or set PWNKIT_MODEL)").option("--verbose", "Show detailed output", false).option("--timeout <ms>", "AI agent timeout in milliseconds", "600000").action(async (repo, opts) => {
60012
+ program3.command("review").description("Deep source code security review of a repository").argument("<repo>", "Local path or git URL to review").option("--depth <depth>", "Review depth: quick, default, deep", "default").option("--format <format>", "Output format: terminal, json, md", "terminal").option("--runtime <runtime>", "Runtime: auto, claude, codex, gemini, api", "auto").option("--db-path <path>", "Path to SQLite database").option("--api-key <key>", "API key for LLM provider (or set OPENROUTER_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY)").option("--model <model>", "LLM model to use (or set PWNKIT_MODEL)").option("--verbose", "Show detailed output", false).option("--timeout <ms>", "AI agent timeout in milliseconds", "600000").action(async (repo, opts) => {
59961
60013
  const depth = opts.depth ?? "default";
59962
60014
  const format = opts.format === "md" ? "markdown" : opts.format;
59963
60015
  const runtime = opts.runtime;
@@ -60026,12 +60078,12 @@ init_source();
60026
60078
  init_dist();
60027
60079
  init_utils();
60028
60080
  function registerAuditCommand(program3) {
60029
- program3.command("audit").description("Audit an npm package for security vulnerabilities").argument("<package>", "npm package name (e.g. lodash, express)").option("--version <version>", "Specific version to audit (default: latest)").option("--depth <depth>", "Audit depth: quick, default, deep", "default").option("--format <format>", "Output format: terminal, json, md", "terminal").option("--runtime <runtime>", "Runtime: auto, claude, codex, gemini, opencode, api", "auto").option("--db-path <path>", "Path to SQLite database").option("--api-key <key>", "API key for LLM provider (or set OPENROUTER_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY)").option("--model <model>", "LLM model to use (or set PWNKIT_MODEL)").option("--verbose", "Show detailed output", false).option("--timeout <ms>", "AI agent timeout in milliseconds", "600000").action(async (packageName, opts) => {
60081
+ program3.command("audit").description("Audit an npm package for security vulnerabilities").argument("<package>", "npm package name (e.g. lodash, express)").option("--version <version>", "Specific version to audit (default: latest)").option("--depth <depth>", "Audit depth: quick, default, deep", "default").option("--format <format>", "Output format: terminal, json, md", "terminal").option("--runtime <runtime>", "Runtime: auto, claude, codex, gemini, api", "auto").option("--db-path <path>", "Path to SQLite database").option("--api-key <key>", "API key for LLM provider (or set OPENROUTER_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY)").option("--model <model>", "LLM model to use (or set PWNKIT_MODEL)").option("--verbose", "Show detailed output", false).option("--timeout <ms>", "AI agent timeout in milliseconds", "600000").action(async (packageName, opts) => {
60030
60082
  const depth = opts.depth ?? "default";
60031
60083
  const format = opts.format === "md" ? "markdown" : opts.format;
60032
60084
  const runtime = opts.runtime;
60033
60085
  const verbose = opts.verbose;
60034
- const validRuntimes = ["api", "claude", "codex", "gemini", "opencode", "auto"];
60086
+ const validRuntimes = ["api", "claude", "codex", "gemini", "auto"];
60035
60087
  if (!validRuntimes.includes(runtime)) {
60036
60088
  console.error(
60037
60089
  source_default.red(`Unknown runtime '${runtime}'. Valid: ${validRuntimes.join(", ")}`)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pwnkit-cli",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.3.1",
5
5
  "description": "AI-powered agentic security scanner. Scan endpoints, audit packages, review source code. Autonomous agents discover, attack, verify, and report.",
6
6
  "bin": {
7
7
  "pwnkit": "dist/index.js"