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.
- package/.github/workflows/ci.yml +23 -0
- package/CHANGELOG.md +96 -0
- package/README.md +193 -0
- package/bootstrap.ps1 +83 -0
- package/bootstrap.sh +71 -0
- package/dist/agent/loop.d.ts +68 -0
- package/dist/agent/loop.d.ts.map +1 -0
- package/dist/agent/loop.js +135 -0
- package/dist/agent/mcp.d.ts +33 -0
- package/dist/agent/mcp.d.ts.map +1 -0
- package/dist/agent/mcp.js +107 -0
- package/dist/agent/session.d.ts +16 -0
- package/dist/agent/session.d.ts.map +1 -0
- package/dist/agent/session.js +55 -0
- package/dist/agent/skills.d.ts +36 -0
- package/dist/agent/skills.d.ts.map +1 -0
- package/dist/agent/skills.js +153 -0
- package/dist/agent/stack.d.ts +21 -0
- package/dist/agent/stack.d.ts.map +1 -0
- package/dist/agent/stack.js +158 -0
- package/dist/agent/task.d.ts +21 -0
- package/dist/agent/task.d.ts.map +1 -0
- package/dist/agent/task.js +45 -0
- package/dist/agent/tools.d.ts +44 -0
- package/dist/agent/tools.d.ts.map +1 -0
- package/dist/agent/tools.js +262 -0
- package/dist/agent/trace.d.ts +34 -0
- package/dist/agent/trace.d.ts.map +1 -0
- package/dist/agent/trace.js +72 -0
- package/dist/agent.d.ts +46 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +103 -0
- package/dist/auth.d.ts +74 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +116 -0
- package/dist/brain/anthropic.d.ts +19 -0
- package/dist/brain/anthropic.d.ts.map +1 -0
- package/dist/brain/anthropic.js +74 -0
- package/dist/brain/claude-cli.d.ts +20 -0
- package/dist/brain/claude-cli.d.ts.map +1 -0
- package/dist/brain/claude-cli.js +79 -0
- package/dist/brain/ghost-ember.d.ts +28 -0
- package/dist/brain/ghost-ember.d.ts.map +1 -0
- package/dist/brain/ghost-ember.js +97 -0
- package/dist/brain/index.d.ts +22 -0
- package/dist/brain/index.d.ts.map +1 -0
- package/dist/brain/index.js +95 -0
- package/dist/brain/openai-compat.d.ts +21 -0
- package/dist/brain/openai-compat.d.ts.map +1 -0
- package/dist/brain/openai-compat.js +119 -0
- package/dist/brain/router/classify.d.ts +23 -0
- package/dist/brain/router/classify.d.ts.map +1 -0
- package/dist/brain/router/classify.js +160 -0
- package/dist/brain/router/execute.d.ts +23 -0
- package/dist/brain/router/execute.d.ts.map +1 -0
- package/dist/brain/router/execute.js +84 -0
- package/dist/brain/router/index.d.ts +26 -0
- package/dist/brain/router/index.d.ts.map +1 -0
- package/dist/brain/router/index.js +118 -0
- package/dist/brain/router/routing-memory.d.ts +27 -0
- package/dist/brain/router/routing-memory.d.ts.map +1 -0
- package/dist/brain/router/routing-memory.js +77 -0
- package/dist/brain/router/select.d.ts +32 -0
- package/dist/brain/router/select.d.ts.map +1 -0
- package/dist/brain/router/select.js +146 -0
- package/dist/brain/router/two-hop.d.ts +23 -0
- package/dist/brain/router/two-hop.d.ts.map +1 -0
- package/dist/brain/router/two-hop.js +39 -0
- package/dist/brain/router/verify.d.ts +37 -0
- package/dist/brain/router/verify.d.ts.map +1 -0
- package/dist/brain/router/verify.js +111 -0
- package/dist/brain/types.d.ts +55 -0
- package/dist/brain/types.d.ts.map +1 -0
- package/dist/brain/types.js +16 -0
- package/dist/brain/worker.d.ts +27 -0
- package/dist/brain/worker.d.ts.map +1 -0
- package/dist/brain/worker.js +71 -0
- package/dist/commands/ai.d.ts +24 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +137 -0
- package/dist/commands/alerts.d.ts +19 -0
- package/dist/commands/alerts.d.ts.map +1 -0
- package/dist/commands/alerts.js +114 -0
- package/dist/commands/billing.d.ts +13 -0
- package/dist/commands/billing.d.ts.map +1 -0
- package/dist/commands/billing.js +55 -0
- package/dist/commands/chat.d.ts +22 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +422 -0
- package/dist/commands/config.d.ts +18 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +136 -0
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +73 -0
- package/dist/commands/global.d.ts +11 -0
- package/dist/commands/global.d.ts.map +1 -0
- package/dist/commands/global.js +253 -0
- package/dist/commands/keep.d.ts +12 -0
- package/dist/commands/keep.d.ts.map +1 -0
- package/dist/commands/keep.js +58 -0
- package/dist/commands/lifecycle.d.ts +17 -0
- package/dist/commands/lifecycle.d.ts.map +1 -0
- package/dist/commands/lifecycle.js +267 -0
- package/dist/commands/login.d.ts +16 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +234 -0
- package/dist/commands/maintenance.d.ts +12 -0
- package/dist/commands/maintenance.d.ts.map +1 -0
- package/dist/commands/maintenance.js +76 -0
- package/dist/commands/mcp.d.ts +16 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +56 -0
- package/dist/commands/memory.d.ts +13 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +218 -0
- package/dist/commands/osint.d.ts +14 -0
- package/dist/commands/osint.d.ts.map +1 -0
- package/dist/commands/osint.js +161 -0
- package/dist/commands/pentest.d.ts +13 -0
- package/dist/commands/pentest.d.ts.map +1 -0
- package/dist/commands/pentest.js +131 -0
- package/dist/commands/scale.d.ts +14 -0
- package/dist/commands/scale.d.ts.map +1 -0
- package/dist/commands/scale.js +191 -0
- package/dist/commands/serve.d.ts +16 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +167 -0
- package/dist/commands/tui.d.ts +17 -0
- package/dist/commands/tui.d.ts.map +1 -0
- package/dist/commands/tui.js +138 -0
- package/dist/commands/wyrm.d.ts +20 -0
- package/dist/commands/wyrm.d.ts.map +1 -0
- package/dist/commands/wyrm.js +274 -0
- package/dist/config.d.ts +67 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +54 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +85 -0
- package/dist/manifest.d.ts +31 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +83 -0
- package/dist/ui.d.ts +57 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +174 -0
- package/dist/utils.d.ts +33 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +155 -0
- package/dist/wyrm/mcp.d.ts +37 -0
- package/dist/wyrm/mcp.d.ts.map +1 -0
- package/dist/wyrm/mcp.js +137 -0
- package/docs/SYSTEM-PREMORTEM.md +397 -0
- package/dragon-manifest.toml +241 -0
- package/dragon.py +177 -0
- package/install/launchd/lk.ghosts.dragonkeep.plist +57 -0
- package/install/systemd/dragonkeep.service +40 -0
- package/media/dragon-silver-lockup.svg +931 -0
- package/media/dragon-silver-mark.svg +931 -0
- package/media/dragon-silver.png +0 -0
- package/package.json +45 -0
- package/specs/001-godmode/constitution.md +54 -0
- package/specs/001-godmode/plan.md +30 -0
- package/specs/001-godmode/spec.md +64 -0
- package/specs/001-godmode/tasks.md +35 -0
- package/specs/002-premortem-positioning/premortem.md +211 -0
- package/src/agent/loop.ts +165 -0
- package/src/agent/mcp.ts +92 -0
- package/src/agent/session.ts +48 -0
- package/src/agent/skills.ts +138 -0
- package/src/agent/stack.ts +154 -0
- package/src/agent/task.ts +55 -0
- package/src/agent/tools.ts +255 -0
- package/src/agent/trace.ts +76 -0
- package/src/agent.ts +114 -0
- package/src/auth.ts +133 -0
- package/src/brain/anthropic.ts +83 -0
- package/src/brain/claude-cli.ts +78 -0
- package/src/brain/ghost-ember.ts +94 -0
- package/src/brain/index.ts +99 -0
- package/src/brain/openai-compat.ts +115 -0
- package/src/brain/router/classify.ts +167 -0
- package/src/brain/router/execute.ts +80 -0
- package/src/brain/router/index.ts +125 -0
- package/src/brain/router/routing-memory.ts +71 -0
- package/src/brain/router/select.ts +156 -0
- package/src/brain/router/two-hop.ts +62 -0
- package/src/brain/router/verify.ts +123 -0
- package/src/brain/types.ts +61 -0
- package/src/brain/worker.ts +72 -0
- package/src/commands/ai.ts +144 -0
- package/src/commands/alerts.ts +131 -0
- package/src/commands/billing.ts +59 -0
- package/src/commands/chat.ts +318 -0
- package/src/commands/config.ts +137 -0
- package/src/commands/doctor.ts +71 -0
- package/src/commands/global.ts +256 -0
- package/src/commands/keep.ts +67 -0
- package/src/commands/lifecycle.ts +273 -0
- package/src/commands/login.ts +184 -0
- package/src/commands/maintenance.ts +54 -0
- package/src/commands/mcp.ts +57 -0
- package/src/commands/memory.ts +229 -0
- package/src/commands/osint.ts +171 -0
- package/src/commands/pentest.ts +140 -0
- package/src/commands/scale.ts +185 -0
- package/src/commands/serve.ts +171 -0
- package/src/commands/tui.ts +126 -0
- package/src/commands/wyrm.ts +269 -0
- package/src/config.ts +93 -0
- package/src/index.ts +92 -0
- package/src/manifest.ts +104 -0
- package/src/ui.ts +188 -0
- package/src/utils.ts +153 -0
- package/src/wyrm/mcp.ts +130 -0
- package/test/auth.test.ts +70 -0
- package/test/brain.test.ts +39 -0
- package/test/security.test.ts +104 -0
- package/test/skills.test.ts +38 -0
- package/test/ui.test.ts +46 -0
- package/tsconfig.json +19 -0
- package/worker/package-lock.json +1527 -0
- package/worker/package.json +17 -0
- package/worker/src/index.ts +76 -0
- package/worker/tsconfig.json +15 -0
- 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
|
+
}
|