ghost-dragon 4.2.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 (226) hide show
  1. package/.github/workflows/ci.yml +23 -0
  2. package/CHANGELOG.md +96 -0
  3. package/README.md +193 -0
  4. package/bootstrap.ps1 +83 -0
  5. package/bootstrap.sh +71 -0
  6. package/dist/agent/loop.d.ts +68 -0
  7. package/dist/agent/loop.d.ts.map +1 -0
  8. package/dist/agent/loop.js +135 -0
  9. package/dist/agent/mcp.d.ts +33 -0
  10. package/dist/agent/mcp.d.ts.map +1 -0
  11. package/dist/agent/mcp.js +107 -0
  12. package/dist/agent/session.d.ts +16 -0
  13. package/dist/agent/session.d.ts.map +1 -0
  14. package/dist/agent/session.js +55 -0
  15. package/dist/agent/skills.d.ts +36 -0
  16. package/dist/agent/skills.d.ts.map +1 -0
  17. package/dist/agent/skills.js +153 -0
  18. package/dist/agent/stack.d.ts +21 -0
  19. package/dist/agent/stack.d.ts.map +1 -0
  20. package/dist/agent/stack.js +158 -0
  21. package/dist/agent/task.d.ts +21 -0
  22. package/dist/agent/task.d.ts.map +1 -0
  23. package/dist/agent/task.js +45 -0
  24. package/dist/agent/tools.d.ts +44 -0
  25. package/dist/agent/tools.d.ts.map +1 -0
  26. package/dist/agent/tools.js +262 -0
  27. package/dist/agent/trace.d.ts +34 -0
  28. package/dist/agent/trace.d.ts.map +1 -0
  29. package/dist/agent/trace.js +72 -0
  30. package/dist/agent.d.ts +46 -0
  31. package/dist/agent.d.ts.map +1 -0
  32. package/dist/agent.js +103 -0
  33. package/dist/auth.d.ts +74 -0
  34. package/dist/auth.d.ts.map +1 -0
  35. package/dist/auth.js +116 -0
  36. package/dist/brain/anthropic.d.ts +19 -0
  37. package/dist/brain/anthropic.d.ts.map +1 -0
  38. package/dist/brain/anthropic.js +74 -0
  39. package/dist/brain/claude-cli.d.ts +20 -0
  40. package/dist/brain/claude-cli.d.ts.map +1 -0
  41. package/dist/brain/claude-cli.js +79 -0
  42. package/dist/brain/ghost-ember.d.ts +28 -0
  43. package/dist/brain/ghost-ember.d.ts.map +1 -0
  44. package/dist/brain/ghost-ember.js +97 -0
  45. package/dist/brain/index.d.ts +22 -0
  46. package/dist/brain/index.d.ts.map +1 -0
  47. package/dist/brain/index.js +95 -0
  48. package/dist/brain/openai-compat.d.ts +21 -0
  49. package/dist/brain/openai-compat.d.ts.map +1 -0
  50. package/dist/brain/openai-compat.js +119 -0
  51. package/dist/brain/router/classify.d.ts +23 -0
  52. package/dist/brain/router/classify.d.ts.map +1 -0
  53. package/dist/brain/router/classify.js +160 -0
  54. package/dist/brain/router/execute.d.ts +23 -0
  55. package/dist/brain/router/execute.d.ts.map +1 -0
  56. package/dist/brain/router/execute.js +84 -0
  57. package/dist/brain/router/index.d.ts +26 -0
  58. package/dist/brain/router/index.d.ts.map +1 -0
  59. package/dist/brain/router/index.js +118 -0
  60. package/dist/brain/router/routing-memory.d.ts +27 -0
  61. package/dist/brain/router/routing-memory.d.ts.map +1 -0
  62. package/dist/brain/router/routing-memory.js +77 -0
  63. package/dist/brain/router/select.d.ts +32 -0
  64. package/dist/brain/router/select.d.ts.map +1 -0
  65. package/dist/brain/router/select.js +146 -0
  66. package/dist/brain/router/two-hop.d.ts +23 -0
  67. package/dist/brain/router/two-hop.d.ts.map +1 -0
  68. package/dist/brain/router/two-hop.js +39 -0
  69. package/dist/brain/router/verify.d.ts +37 -0
  70. package/dist/brain/router/verify.d.ts.map +1 -0
  71. package/dist/brain/router/verify.js +111 -0
  72. package/dist/brain/types.d.ts +55 -0
  73. package/dist/brain/types.d.ts.map +1 -0
  74. package/dist/brain/types.js +16 -0
  75. package/dist/brain/worker.d.ts +27 -0
  76. package/dist/brain/worker.d.ts.map +1 -0
  77. package/dist/brain/worker.js +71 -0
  78. package/dist/commands/ai.d.ts +24 -0
  79. package/dist/commands/ai.d.ts.map +1 -0
  80. package/dist/commands/ai.js +137 -0
  81. package/dist/commands/alerts.d.ts +19 -0
  82. package/dist/commands/alerts.d.ts.map +1 -0
  83. package/dist/commands/alerts.js +114 -0
  84. package/dist/commands/billing.d.ts +13 -0
  85. package/dist/commands/billing.d.ts.map +1 -0
  86. package/dist/commands/billing.js +55 -0
  87. package/dist/commands/chat.d.ts +22 -0
  88. package/dist/commands/chat.d.ts.map +1 -0
  89. package/dist/commands/chat.js +422 -0
  90. package/dist/commands/config.d.ts +18 -0
  91. package/dist/commands/config.d.ts.map +1 -0
  92. package/dist/commands/config.js +136 -0
  93. package/dist/commands/doctor.d.ts +11 -0
  94. package/dist/commands/doctor.d.ts.map +1 -0
  95. package/dist/commands/doctor.js +73 -0
  96. package/dist/commands/global.d.ts +11 -0
  97. package/dist/commands/global.d.ts.map +1 -0
  98. package/dist/commands/global.js +253 -0
  99. package/dist/commands/keep.d.ts +12 -0
  100. package/dist/commands/keep.d.ts.map +1 -0
  101. package/dist/commands/keep.js +58 -0
  102. package/dist/commands/lifecycle.d.ts +17 -0
  103. package/dist/commands/lifecycle.d.ts.map +1 -0
  104. package/dist/commands/lifecycle.js +267 -0
  105. package/dist/commands/login.d.ts +16 -0
  106. package/dist/commands/login.d.ts.map +1 -0
  107. package/dist/commands/login.js +234 -0
  108. package/dist/commands/maintenance.d.ts +12 -0
  109. package/dist/commands/maintenance.d.ts.map +1 -0
  110. package/dist/commands/maintenance.js +76 -0
  111. package/dist/commands/mcp.d.ts +16 -0
  112. package/dist/commands/mcp.d.ts.map +1 -0
  113. package/dist/commands/mcp.js +56 -0
  114. package/dist/commands/memory.d.ts +13 -0
  115. package/dist/commands/memory.d.ts.map +1 -0
  116. package/dist/commands/memory.js +218 -0
  117. package/dist/commands/osint.d.ts +14 -0
  118. package/dist/commands/osint.d.ts.map +1 -0
  119. package/dist/commands/osint.js +161 -0
  120. package/dist/commands/pentest.d.ts +13 -0
  121. package/dist/commands/pentest.d.ts.map +1 -0
  122. package/dist/commands/pentest.js +131 -0
  123. package/dist/commands/scale.d.ts +14 -0
  124. package/dist/commands/scale.d.ts.map +1 -0
  125. package/dist/commands/scale.js +191 -0
  126. package/dist/commands/serve.d.ts +16 -0
  127. package/dist/commands/serve.d.ts.map +1 -0
  128. package/dist/commands/serve.js +167 -0
  129. package/dist/commands/tui.d.ts +17 -0
  130. package/dist/commands/tui.d.ts.map +1 -0
  131. package/dist/commands/tui.js +138 -0
  132. package/dist/commands/wyrm.d.ts +20 -0
  133. package/dist/commands/wyrm.d.ts.map +1 -0
  134. package/dist/commands/wyrm.js +274 -0
  135. package/dist/config.d.ts +67 -0
  136. package/dist/config.d.ts.map +1 -0
  137. package/dist/config.js +54 -0
  138. package/dist/index.d.ts +16 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +85 -0
  141. package/dist/manifest.d.ts +31 -0
  142. package/dist/manifest.d.ts.map +1 -0
  143. package/dist/manifest.js +83 -0
  144. package/dist/ui.d.ts +57 -0
  145. package/dist/ui.d.ts.map +1 -0
  146. package/dist/ui.js +174 -0
  147. package/dist/utils.d.ts +33 -0
  148. package/dist/utils.d.ts.map +1 -0
  149. package/dist/utils.js +155 -0
  150. package/dist/wyrm/mcp.d.ts +37 -0
  151. package/dist/wyrm/mcp.d.ts.map +1 -0
  152. package/dist/wyrm/mcp.js +137 -0
  153. package/docs/SYSTEM-PREMORTEM.md +397 -0
  154. package/dragon-manifest.toml +241 -0
  155. package/dragon.py +177 -0
  156. package/install/launchd/lk.ghosts.dragonkeep.plist +57 -0
  157. package/install/systemd/dragonkeep.service +40 -0
  158. package/media/dragon-silver-lockup.svg +931 -0
  159. package/media/dragon-silver-mark.svg +931 -0
  160. package/media/dragon-silver.png +0 -0
  161. package/package.json +45 -0
  162. package/specs/001-godmode/constitution.md +54 -0
  163. package/specs/001-godmode/plan.md +30 -0
  164. package/specs/001-godmode/spec.md +64 -0
  165. package/specs/001-godmode/tasks.md +35 -0
  166. package/specs/002-premortem-positioning/premortem.md +211 -0
  167. package/src/agent/loop.ts +165 -0
  168. package/src/agent/mcp.ts +92 -0
  169. package/src/agent/session.ts +48 -0
  170. package/src/agent/skills.ts +138 -0
  171. package/src/agent/stack.ts +154 -0
  172. package/src/agent/task.ts +55 -0
  173. package/src/agent/tools.ts +255 -0
  174. package/src/agent/trace.ts +76 -0
  175. package/src/agent.ts +114 -0
  176. package/src/auth.ts +133 -0
  177. package/src/brain/anthropic.ts +83 -0
  178. package/src/brain/claude-cli.ts +78 -0
  179. package/src/brain/ghost-ember.ts +94 -0
  180. package/src/brain/index.ts +99 -0
  181. package/src/brain/openai-compat.ts +115 -0
  182. package/src/brain/router/classify.ts +167 -0
  183. package/src/brain/router/execute.ts +80 -0
  184. package/src/brain/router/index.ts +125 -0
  185. package/src/brain/router/routing-memory.ts +71 -0
  186. package/src/brain/router/select.ts +156 -0
  187. package/src/brain/router/two-hop.ts +62 -0
  188. package/src/brain/router/verify.ts +123 -0
  189. package/src/brain/types.ts +61 -0
  190. package/src/brain/worker.ts +72 -0
  191. package/src/commands/ai.ts +144 -0
  192. package/src/commands/alerts.ts +131 -0
  193. package/src/commands/billing.ts +59 -0
  194. package/src/commands/chat.ts +318 -0
  195. package/src/commands/config.ts +137 -0
  196. package/src/commands/doctor.ts +71 -0
  197. package/src/commands/global.ts +256 -0
  198. package/src/commands/keep.ts +67 -0
  199. package/src/commands/lifecycle.ts +273 -0
  200. package/src/commands/login.ts +184 -0
  201. package/src/commands/maintenance.ts +54 -0
  202. package/src/commands/mcp.ts +57 -0
  203. package/src/commands/memory.ts +229 -0
  204. package/src/commands/osint.ts +171 -0
  205. package/src/commands/pentest.ts +140 -0
  206. package/src/commands/scale.ts +185 -0
  207. package/src/commands/serve.ts +171 -0
  208. package/src/commands/tui.ts +126 -0
  209. package/src/commands/wyrm.ts +269 -0
  210. package/src/config.ts +93 -0
  211. package/src/index.ts +92 -0
  212. package/src/manifest.ts +104 -0
  213. package/src/ui.ts +188 -0
  214. package/src/utils.ts +153 -0
  215. package/src/wyrm/mcp.ts +130 -0
  216. package/test/auth.test.ts +70 -0
  217. package/test/brain.test.ts +39 -0
  218. package/test/security.test.ts +104 -0
  219. package/test/skills.test.ts +38 -0
  220. package/test/ui.test.ts +46 -0
  221. package/tsconfig.json +19 -0
  222. package/worker/package-lock.json +1527 -0
  223. package/worker/package.json +17 -0
  224. package/worker/src/index.ts +76 -0
  225. package/worker/tsconfig.json +15 -0
  226. package/worker/wrangler.toml +26 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * dragon memory — Phantom Memory (the moat)
3
+ *
4
+ * - memory list — every known target with summary
5
+ * - memory show <target> — full dossier
6
+ * - memory context <target> — plaintext brain-context prompt
7
+ * - memory reindex — rebuild memory from reports/
8
+ * - memory verify — diff reports/ vs Phantom Memory
9
+ */
10
+
11
+ import type { Command } from 'commander'
12
+ import type { DragonConfig } from '../config.js'
13
+ import { fetchJSON, label, success, error, info, warn, table } from '../utils.js'
14
+ import chalk from 'chalk'
15
+
16
+ function api(config: DragonConfig, path: string): string {
17
+ const port = config.products.pentest.controlPort ?? 4091
18
+ return `http://localhost:${port}${path}`
19
+ }
20
+
21
+ interface TargetSummary {
22
+ display: string
23
+ scan_count: number
24
+ last_scan_at: string | null
25
+ latest_findings: number | null
26
+ latest_risk: string | null
27
+ osint_count: number
28
+ entity_count: number
29
+ cred_count: number
30
+ last_seen_at: string
31
+ }
32
+
33
+ export function registerMemoryCommands(program: Command, config: DragonConfig) {
34
+ const mem = program.command('memory').description('Phantom Memory — persistent engagement memory')
35
+
36
+ // --- list ---
37
+ mem
38
+ .command('list')
39
+ .description('List every target Phantom Memory has ever touched')
40
+ .action(async () => {
41
+ console.log(label('Phantom Memory'), 'Known targets:\n')
42
+ try {
43
+ const d = await fetchJSON<{ targets: TargetSummary[] }>(api(config, '/v1/memory'))
44
+ if (d.targets.length === 0) {
45
+ info('No targets in memory yet — run a scan to seed.')
46
+ return
47
+ }
48
+ table(d.targets.map((t) => ({
49
+ Target: t.display,
50
+ Scans: String(t.scan_count),
51
+ Findings: String(t.latest_findings ?? '—'),
52
+ Risk: t.latest_risk ?? '—',
53
+ OSINT: String(t.osint_count),
54
+ Entities: String(t.entity_count),
55
+ Creds: String(t.cred_count),
56
+ 'Last seen': new Date(t.last_seen_at).toLocaleDateString(),
57
+ })))
58
+ } catch (e) {
59
+ error('Could not reach control_api. Is PhantomDragon Control running? `dragon serve`')
60
+ info(String(e))
61
+ }
62
+ })
63
+
64
+ // --- show ---
65
+ mem
66
+ .command('show <target>')
67
+ .description('Full memory dossier on a target')
68
+ .action(async (target) => {
69
+ console.log(label('Phantom Memory'), `Dossier on ${chalk.white(target)}\n`)
70
+ try {
71
+ const d: any = await fetchJSON<any>(api(config, `/v1/memory/target/${encodeURIComponent(target)}`))
72
+ if (!d.exists) {
73
+ info(`No memory for ${target} yet.`)
74
+ return
75
+ }
76
+ const s = d.summary
77
+ info(`First seen ${d.first_seen_at}, last touched ${d.last_seen_at}`)
78
+ info(`${s.scan_count} scans · ${s.persistent_finding_count} persistent findings · ${s.tech_count} techs · ${s.investigation_count} OSINT investigations · ${s.entity_count} entities · ${s.credential_exposure_count} cred exposures\n`)
79
+
80
+ if (d.scans?.length) {
81
+ console.log(chalk.dim('── Scans ──'))
82
+ table(d.scans.slice(0, 5).map((sc: any) => ({
83
+ ID: sc.scan_id.slice(-22),
84
+ Mode: sc.mode || '?',
85
+ Findings: String(sc.total_findings),
86
+ Critical: String(sc.severity_counts?.CRITICAL ?? 0),
87
+ High: String(sc.severity_counts?.HIGH ?? 0),
88
+ Risk: sc.risk_rating ?? '?',
89
+ Started: sc.started_at?.slice(0, 16) ?? '?',
90
+ })))
91
+ console.log()
92
+ }
93
+ if (d.persistent_findings?.length) {
94
+ console.log(chalk.dim('── Persistent findings (≥2 scans) ──'))
95
+ d.persistent_findings.slice(0, 10).forEach((f: any) => {
96
+ console.log(` ${chalk.bold(f.severity?.padEnd(8))} ${f.title} ${chalk.dim('· ' + f.category)}`)
97
+ })
98
+ console.log()
99
+ }
100
+ if (d.new_findings?.length) {
101
+ console.log(chalk.red('── Regressions (new in latest scan) ──'))
102
+ d.new_findings.slice(0, 8).forEach((f: any) => {
103
+ console.log(` ${chalk.red(f.severity?.padEnd(8))} ${f.title}`)
104
+ })
105
+ console.log()
106
+ }
107
+ if (d.resolved_findings?.length) {
108
+ console.log(chalk.green('── Resolved (vs prior scan) ──'))
109
+ d.resolved_findings.slice(0, 8).forEach((f: any) => {
110
+ console.log(` ${chalk.green(f.severity?.padEnd(8))} ${f.title}`)
111
+ })
112
+ console.log()
113
+ }
114
+ if (d.tech_observations?.length) {
115
+ console.log(chalk.dim('── Tech observations ──'))
116
+ console.log(' ' + d.tech_observations.map((t: any) => `${t.tech}×${t.occurrences}`).join(', '))
117
+ console.log()
118
+ }
119
+ if (d.osint_entity_type_counts && Object.keys(d.osint_entity_type_counts).length) {
120
+ console.log(chalk.dim('── OSINT entities by type ──'))
121
+ console.log(' ' + Object.entries(d.osint_entity_type_counts)
122
+ .sort((a: any, b: any) => b[1] - a[1])
123
+ .map(([t, n]: any) => `${t}×${n}`).join(', '))
124
+ }
125
+ } catch (e) {
126
+ error('Fetch failed.')
127
+ info(String(e))
128
+ }
129
+ })
130
+
131
+ // --- context ---
132
+ mem
133
+ .command('context <target>')
134
+ .description('Plaintext brain-context prompt for the AI brain')
135
+ .action(async (target) => {
136
+ try {
137
+ const port = config.products.pentest.controlPort ?? 4091
138
+ const res = await fetch(`http://localhost:${port}/v1/memory/target/${encodeURIComponent(target)}/context`)
139
+ if (!res.ok) {
140
+ error(`HTTP ${res.status}`); return
141
+ }
142
+ const txt = await res.text()
143
+ console.log(txt)
144
+ } catch (e) {
145
+ error('Could not reach control_api.')
146
+ info(String(e))
147
+ }
148
+ })
149
+
150
+ // --- reindex ---
151
+ mem
152
+ .command('reindex')
153
+ .description('Rebuild Phantom Memory from reports/ directory')
154
+ .action(async () => {
155
+ console.log(label('Phantom Memory'), 'Reindexing...\n')
156
+ try {
157
+ const port = config.products.pentest.controlPort ?? 4091
158
+ const res = await fetch(`http://localhost:${port}/v1/memory/reindex`, { method: 'POST' })
159
+ if (!res.ok) { error(`HTTP ${res.status}`); return }
160
+ const d = await res.json() as { scans_ingested: number; skipped: number }
161
+ success(`Ingested ${d.scans_ingested} scans (skipped ${d.skipped})`)
162
+ } catch (e) {
163
+ error('Reindex failed.')
164
+ info(String(e))
165
+ }
166
+ })
167
+
168
+ // --- copilot ---
169
+ mem
170
+ .command('copilot <target>')
171
+ .description('What next? — rule-based suggestions from Phantom Memory state')
172
+ .action(async (target) => {
173
+ try {
174
+ const d = await fetchJSON<{ suggestions: Array<{ verb: string; title: string; rationale: string; command: string }> }>(
175
+ api(config, `/v1/memory/target/${encodeURIComponent(target)}/copilot`),
176
+ )
177
+ console.log(label('Dragon Copilot'), `Next actions for ${chalk.white(target)}:\n`)
178
+ d.suggestions.forEach((s) => {
179
+ const icon = s.verb === 'alert' ? chalk.red('▲') :
180
+ s.verb === 'scan' ? chalk.green('▶') :
181
+ s.verb === 'osint' ? chalk.cyan('🔍') :
182
+ chalk.dim('●')
183
+ console.log(` ${icon} ${chalk.bold(s.title)}`)
184
+ console.log(` ${chalk.dim(s.rationale)}`)
185
+ console.log(` ${chalk.cyan('$')} ${s.command}\n`)
186
+ })
187
+ } catch (e) {
188
+ error('Copilot fetch failed.')
189
+ info(String(e))
190
+ }
191
+ })
192
+
193
+ // --- brief ---
194
+ mem
195
+ .command('brief <target>')
196
+ .description('Generate client-deliverable engagement brief (Markdown)')
197
+ .option('--as-of <date>', 'Time-travel: brief as of given ISO date')
198
+ .action(async (target, opts) => {
199
+ try {
200
+ const port = config.products.pentest.controlPort ?? 4091
201
+ const params = new URLSearchParams()
202
+ if (opts.asOf) params.set('as_of', opts.asOf)
203
+ const url = `http://localhost:${port}/v1/memory/target/${encodeURIComponent(target)}/brief${params.size ? `?${params}` : ''}`
204
+ const res = await fetch(url)
205
+ if (!res.ok) { error(`HTTP ${res.status}`); return }
206
+ const md = await res.text()
207
+ process.stdout.write(md)
208
+ } catch (e) {
209
+ error('Brief fetch failed.')
210
+ info(String(e))
211
+ }
212
+ })
213
+
214
+ // --- verify ---
215
+ mem
216
+ .command('verify')
217
+ .description('Diff reports/ vs Phantom Memory (TODO: full implementation; for now triggers reindex)')
218
+ .action(async () => {
219
+ warn('Full diff verification is a deferred feature. Triggering reindex as the safe surrogate.')
220
+ try {
221
+ const port = config.products.pentest.controlPort ?? 4091
222
+ const res = await fetch(`http://localhost:${port}/v1/memory/reindex`, { method: 'POST' })
223
+ const d = await res.json() as { scans_ingested: number; skipped: number }
224
+ info(`Reindex result: ${d.scans_ingested} scans ingested, ${d.skipped} skipped.`)
225
+ } catch (e) {
226
+ error(String(e))
227
+ }
228
+ })
229
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * dragon osint — DragonNet OSINT Investigation Platform
3
+ *
4
+ * - osint open <target> — open a new investigation seeded with target
5
+ * - osint list — list known investigations
6
+ * - osint show <id> — fetch entities + edges for an investigation
7
+ * - osint search <q> — cross-investigation entity search
8
+ * - osint serve — start the DragonNet API + dashboard
9
+ * - osint legal-posture <jdx> — print per-collector legal posture
10
+ */
11
+
12
+ import type { Command } from 'commander'
13
+ import type { DragonConfig } from '../config.js'
14
+ import { getProductPath } from '../config.js'
15
+ import { run, fetchJSON, label, success, error, info, warn, table } from '../utils.js'
16
+ import chalk from 'chalk'
17
+
18
+ function api(config: DragonConfig, path: string): string {
19
+ const port = config.products.net.apiPort ?? 4080
20
+ return `http://localhost:${port}${path}`
21
+ }
22
+
23
+ function ui(config: DragonConfig, path: string): string {
24
+ const port = config.products.net.uiPort ?? 4081
25
+ return `http://localhost:${port}${path}`
26
+ }
27
+
28
+ const LEGAL_POSTURES: Record<string, Record<string, string>> = {
29
+ lk: {
30
+ TORWATCH: "defensible if seeds are public services and operator records authorization (CCA 2007)",
31
+ PASTEDRAGNET: "defensible (public paste sites only)",
32
+ TELEGAZER: "defensible (public previews only); GDPR if EU subjects appear",
33
+ NIGHTGLASS: "operator-bears-responsibility; corpus provenance required",
34
+ },
35
+ us: {
36
+ TORWATCH: "defensible under CFAA if no authentication is bypassed; document seeds",
37
+ PASTEDRAGNET: "defensible (public surface)",
38
+ TELEGAZER: "defensible (public previews)",
39
+ NIGHTGLASS: "operator-bears-responsibility; possessing breach data is grey-area",
40
+ },
41
+ eu: {
42
+ TORWATCH: "defensible; GDPR data-controller obligations apply on EU subjects",
43
+ PASTEDRAGNET: "defensible; honour right-to-erasure",
44
+ TELEGAZER: "defensible; GDPR data-controller obligations apply",
45
+ NIGHTGLASS: "operator-bears-responsibility; GDPR retention rules apply",
46
+ },
47
+ }
48
+
49
+ export function registerOsintCommands(program: Command, config: DragonConfig) {
50
+ const osint = program.command('osint').description('DragonNet — OSINT investigations (no external API deps)')
51
+
52
+ // --- open ---
53
+ osint
54
+ .command('open <target>')
55
+ .description('Open a new DragonNet investigation seeded with the target (opens browser)')
56
+ .action(async (target) => {
57
+ const url = ui(config, `/?seed_target=${encodeURIComponent(target)}&autocreate=1`)
58
+ console.log(label('DragonNet'), `Opening investigation seeded with ${chalk.white(target)}`)
59
+ console.log(` → ${chalk.dim(url)}`)
60
+ // Best-effort open in browser
61
+ const opener = process.platform === 'darwin' ? 'open' :
62
+ process.platform === 'win32' ? 'start' : 'xdg-open'
63
+ try { await run(opener, [url], process.cwd()) } catch { /* no opener */ }
64
+ success('Browser launched (or URL printed)')
65
+ })
66
+
67
+ // --- list ---
68
+ osint
69
+ .command('list')
70
+ .description('List investigations (requires DragonNet API auth via browser session)')
71
+ .action(async () => {
72
+ console.log(label('DragonNet'), 'Investigations:\n')
73
+ try {
74
+ const d = await fetchJSON<{ investigations: Array<{ id: string; name: string; status: string; created_at: number }> }>(
75
+ api(config, '/v1/investigations'),
76
+ )
77
+ if (d.investigations.length === 0) {
78
+ info('No investigations yet — try: dragon osint open <target>')
79
+ return
80
+ }
81
+ table(d.investigations.map((i) => ({
82
+ ID: i.id.slice(-12),
83
+ Name: i.name,
84
+ Status: i.status,
85
+ Created: new Date(i.created_at).toLocaleDateString(),
86
+ })))
87
+ } catch (e) {
88
+ error('Could not reach DragonNet API. Is it running? `dragon osint serve`')
89
+ info(String(e))
90
+ }
91
+ })
92
+
93
+ // --- show ---
94
+ osint
95
+ .command('show <id>')
96
+ .description('Show an investigation graph summary (entities + edges)')
97
+ .action(async (id) => {
98
+ console.log(label('DragonNet'), `Investigation ${chalk.white(id.slice(-8))}\n`)
99
+ try {
100
+ const d = await fetchJSON<{ entities: any[]; edges: any[]; counts: { entities: number; edges: number } }>(
101
+ api(config, `/v1/entities/investigation/${id}/graph`),
102
+ )
103
+ info(`Entities: ${d.counts.entities} Edges: ${d.counts.edges}`)
104
+ const byType: Record<string, number> = {}
105
+ for (const e of d.entities) byType[e.type] = (byType[e.type] ?? 0) + 1
106
+ Object.entries(byType).sort((a, b) => b[1] - a[1]).forEach(([t, n]) => {
107
+ console.log(` ${chalk.green(t.padEnd(20))} ${n}`)
108
+ })
109
+ } catch (e) {
110
+ error('Fetch failed (auth required, or DragonNet API offline).')
111
+ info(String(e))
112
+ }
113
+ })
114
+
115
+ // --- search ---
116
+ osint
117
+ .command('search <q>')
118
+ .description('Cross-investigation entity search')
119
+ .option('-t, --type <type>', 'Filter by entity type')
120
+ .action(async (q, opts) => {
121
+ const params = new URLSearchParams({ q, limit: '50' })
122
+ if (opts.type) params.set('type', opts.type)
123
+ console.log(label('DragonNet'), `Searching for: ${chalk.white(q)}\n`)
124
+ try {
125
+ const d = await fetchJSON<{ results: any[] }>(
126
+ api(config, `/v1/entities/search?${params}`),
127
+ )
128
+ if (d.results.length === 0) {
129
+ info('No hits')
130
+ return
131
+ }
132
+ table(d.results.map((r: any) => ({
133
+ Type: r.type,
134
+ Name: r.display_name,
135
+ Investigation: r.investigation_name,
136
+ Created: new Date(r.created_at).toLocaleDateString(),
137
+ })))
138
+ } catch (e) {
139
+ error('Search failed (auth required, or DragonNet API offline).')
140
+ info(String(e))
141
+ }
142
+ })
143
+
144
+ // --- serve ---
145
+ osint
146
+ .command('serve')
147
+ .description('Start DragonNet API + dashboard (foreground; Ctrl-C to stop)')
148
+ .action(async () => {
149
+ const path = getProductPath(config, 'net')
150
+ console.log(label('DragonNet'), 'Starting API + dashboard...\n')
151
+ info(`API: http://localhost:${config.products.net.apiPort}`)
152
+ info(`Dashboard: http://localhost:${config.products.net.uiPort}\n`)
153
+ await run('pnpm', ['dev'], path)
154
+ })
155
+
156
+ // --- legal-posture ---
157
+ osint
158
+ .command('legal-posture [jurisdiction]')
159
+ .description('Print per-collector legal posture for a jurisdiction (lk|us|eu)')
160
+ .action((jurisdiction = 'lk') => {
161
+ const jdx = jurisdiction.toLowerCase()
162
+ const postures = LEGAL_POSTURES[jdx]
163
+ if (!postures) {
164
+ error(`Unknown jurisdiction "${jdx}". Supported: ${Object.keys(LEGAL_POSTURES).join(', ')}`)
165
+ return
166
+ }
167
+ console.log(label('Legal Posture'), `Jurisdiction: ${chalk.white(jdx.toUpperCase())}\n`)
168
+ table(Object.entries(postures).map(([k, v]) => ({ Collector: k, Posture: v })))
169
+ warn('This is not legal advice. The operator is responsible for compliance in their jurisdiction.')
170
+ })
171
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * dragon pentest — PhantomDragon Penetration Testing
3
+ *
4
+ * Manages security scans:
5
+ * - Run scans against targets
6
+ * - View/export reports
7
+ * - Update payloads
8
+ * - Configure scan profiles
9
+ */
10
+
11
+ import type { Command } from 'commander'
12
+ import type { DragonConfig } from '../config.js'
13
+ import { getProductPath } from '../config.js'
14
+ import { run, label, success, error, info } from '../utils.js'
15
+ import chalk from 'chalk'
16
+ import { existsSync, readdirSync } from 'fs'
17
+ import { join } from 'path'
18
+
19
+ export function registerPentestCommands(program: Command, config: DragonConfig) {
20
+ const pentest = program
21
+ .command('pentest')
22
+ .description('PhantomDragon — Web application security scanner')
23
+
24
+ // --- scan ---
25
+ pentest
26
+ .command('scan <target>')
27
+ .description('Run a security scan against a target URL')
28
+ .option('-p, --profile <profile>', 'Scan profile: quick|standard|full|api_only|auth_only', 'standard')
29
+ .option('-m, --mode <mode>', 'Scan mode: safe|standard|aggressive|chaos', 'safe')
30
+ .option('-o, --output <dir>', 'Output directory for reports')
31
+ .option('--no-spider', 'Skip crawling/spidering')
32
+ .action(async (target, opts) => {
33
+ const path = getProductPath(config, 'pentest')
34
+ console.log(label('PhantomDragon'), `Scanning ${chalk.white(target)}`)
35
+ console.log(` Profile: ${chalk.dim(opts.profile)} Mode: ${chalk.dim(opts.mode)}\n`)
36
+
37
+ const args = ['phantomdragon.py', '-t', target, '--profile', opts.profile, '--mode', opts.mode]
38
+ if (opts.output) args.push('-o', opts.output)
39
+ if (opts.noSpider === false) args.push('--no-spider')
40
+
41
+ const code = await run('python3', args, path)
42
+ code === 0 ? success('Scan complete') : error(`Scan failed (exit ${code})`)
43
+ })
44
+
45
+ // --- quick ---
46
+ pentest
47
+ .command('quick <target>')
48
+ .description('Quick scan (shortcut for --profile quick --mode safe)')
49
+ .action(async (target) => {
50
+ const path = getProductPath(config, 'pentest')
51
+ console.log(label('PhantomDragon'), `Quick scan: ${chalk.white(target)}\n`)
52
+ const code = await run('python3', ['phantomdragon.py', '-t', target, '--profile', 'quick', '--mode', 'safe'], path)
53
+ code === 0 ? success('Quick scan complete') : error(`Scan failed (exit ${code})`)
54
+ })
55
+
56
+ // --- reports ---
57
+ pentest
58
+ .command('reports')
59
+ .description('List generated reports')
60
+ .action(async () => {
61
+ const path = getProductPath(config, 'pentest')
62
+ const reportsDir = join(path, 'reports')
63
+ if (!existsSync(reportsDir)) {
64
+ info('No reports directory')
65
+ return
66
+ }
67
+ console.log(label('PhantomDragon'), 'Reports:\n')
68
+ const entries = readdirSync(reportsDir, { withFileTypes: true })
69
+ const dirs = entries.filter(e => e.isDirectory()).sort((a, b) => b.name.localeCompare(a.name))
70
+ const files = entries.filter(e => e.isFile() && e.name.endsWith('.md'))
71
+
72
+ for (const d of dirs) {
73
+ console.log(` ${chalk.green('📁')} ${d.name}`)
74
+ }
75
+ for (const f of files) {
76
+ console.log(` ${chalk.dim('📄')} ${f.name}`)
77
+ }
78
+ if (dirs.length === 0 && files.length === 0) {
79
+ info('No reports found')
80
+ }
81
+ })
82
+
83
+ // --- payloads ---
84
+ pentest
85
+ .command('payloads')
86
+ .description('Show payload statistics')
87
+ .action(async () => {
88
+ const path = getProductPath(config, 'pentest')
89
+ const payloadsDir = join(path, 'payloads')
90
+ if (!existsSync(payloadsDir)) {
91
+ info('No payloads directory')
92
+ return
93
+ }
94
+ console.log(label('PhantomDragon'), 'Payload inventory:\n')
95
+ const categories = readdirSync(payloadsDir, { withFileTypes: true }).filter(e => e.isDirectory())
96
+ const { readFileSync: rf } = await import('fs')
97
+ let total = 0
98
+ for (const cat of categories) {
99
+ const catPath = join(payloadsDir, cat.name)
100
+ const files = readdirSync(catPath).filter(f => f.endsWith('.txt') || f.endsWith('.json'))
101
+ let count = 0
102
+ for (const f of files) {
103
+ try {
104
+ // Safe line count — no shell interpolation of filename
105
+ const body = rf(join(catPath, f), 'utf-8')
106
+ count += body.length === 0 ? 0 : body.split('\n').length
107
+ } catch { /* skip */ }
108
+ }
109
+ total += count
110
+ console.log(` ${chalk.green(cat.name.padEnd(20))} ${chalk.white(String(count).padStart(5))} payloads ${chalk.dim(`(${files.length} files)`)}`)
111
+ }
112
+ console.log(chalk.dim(`\n Total: ${total} payloads`))
113
+ })
114
+
115
+ // --- config ---
116
+ pentest
117
+ .command('config')
118
+ .description('Show current scan configuration')
119
+ .action(async () => {
120
+ const path = getProductPath(config, 'pentest')
121
+ const configFile = join(path, 'config/pentest.json')
122
+ if (!existsSync(configFile)) {
123
+ error('Config not found: config/pentest.json')
124
+ return
125
+ }
126
+ console.log(label('PhantomDragon'), 'Configuration:\n')
127
+ await run('cat', [configFile], path)
128
+ })
129
+
130
+ // --- update ---
131
+ pentest
132
+ .command('update')
133
+ .description('Update PhantomDragon (git pull)')
134
+ .action(async () => {
135
+ const path = getProductPath(config, 'pentest')
136
+ console.log(label('PhantomDragon'), 'Updating...\n')
137
+ const code = await run('git', ['pull', '--rebase'], path)
138
+ code === 0 ? success('Updated') : error(`Update failed (exit ${code})`)
139
+ })
140
+ }