@naraya/cli 0.1.0 → 0.4.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/LICENSE +20 -0
- package/README.md +184 -93
- package/bin/naraya-native.mjs +4 -0
- package/bin/naraya.mjs +1 -142
- package/bin/undici-timeout.mjs +1 -0
- package/dist/assets.pack.gz +0 -0
- package/dist/mcp/config-loader.js +32 -0
- package/dist/mcp/lifecycle.js +90 -0
- package/dist/mcp/tool-mapper.js +31 -0
- package/dist/mcp/transport.js +30 -0
- package/dist/pentest/catalog/catalog-loader.js +45 -0
- package/dist/pentest/catalog/index.js +1 -0
- package/dist/pentest/cli.js +117 -0
- package/dist/pentest/command-builder/command-builder.js +90 -0
- package/dist/pentest/command-builder/index.js +1 -0
- package/dist/pentest/index.js +10 -0
- package/dist/pentest/installer/index.js +1 -0
- package/dist/pentest/installer/tool-installer.js +90 -0
- package/dist/pentest/manager.js +125 -0
- package/dist/pentest/mode/index.js +1 -0
- package/dist/pentest/mode/mode-selector.js +127 -0
- package/dist/pentest/selector/index.js +1 -0
- package/dist/pentest/selector/tool-selector.js +66 -0
- package/dist/pentest/skill-bridge/index.js +1 -0
- package/dist/pentest/skill-bridge/skill-bridge.js +66 -0
- package/dist/pentest/skills/generator/index.js +1 -0
- package/dist/pentest/skills/generator/skill-generator.js +310 -0
- package/dist/pentest/skills/index.js +3 -0
- package/dist/pentest/skills/loader/index.js +1 -0
- package/dist/pentest/skills/loader/skill-loader.js +167 -0
- package/dist/pentest/skills/register/index.js +1 -0
- package/dist/pentest/skills/register/skill-register.js +162 -0
- package/dist/pentest/skills/types.js +1 -0
- package/dist/pentest/types.js +90 -0
- package/package.json +42 -14
- package/src/assets-pack.mjs +1 -0
- package/src/banner.mjs +5 -0
- package/src/clipboard.mjs +1 -0
- package/src/config.mjs +1 -40
- package/src/goodbye.mjs +7 -0
- package/src/login.mjs +7 -49
- package/src/mcp/config-loader.ts +50 -0
- package/src/mcp/lifecycle.ts +113 -0
- package/src/mcp/tool-mapper.ts +42 -0
- package/src/mcp/transport.ts +38 -0
- package/src/mcp-cli.mjs +5 -0
- package/src/pentest/catalog/catalog-loader.ts +55 -0
- package/src/pentest/catalog/index.ts +1 -0
- package/src/pentest/cli.ts +130 -0
- package/src/pentest/command-builder/command-builder.ts +109 -0
- package/src/pentest/command-builder/index.ts +1 -0
- package/src/pentest/index.ts +11 -0
- package/src/pentest/installer/index.ts +1 -0
- package/src/pentest/installer/tool-installer.ts +107 -0
- package/src/pentest/manager.ts +167 -0
- package/src/pentest/mode/index.ts +1 -0
- package/src/pentest/mode/mode-selector.ts +159 -0
- package/src/pentest/selector/index.ts +1 -0
- package/src/pentest/selector/tool-selector.ts +87 -0
- package/src/pentest/skill-bridge/index.ts +1 -0
- package/src/pentest/skill-bridge/skill-bridge.ts +86 -0
- package/src/pentest/skills/generator/index.ts +1 -0
- package/src/pentest/skills/generator/skill-generator.ts +373 -0
- package/src/pentest/skills/index.ts +4 -0
- package/src/pentest/skills/loader/index.ts +1 -0
- package/src/pentest/skills/loader/skill-loader.ts +206 -0
- package/src/pentest/skills/register/index.ts +1 -0
- package/src/pentest/skills/register/skill-register.ts +196 -0
- package/src/pentest/skills/types.ts +66 -0
- package/src/pentest/types.ts +341 -0
- package/src/seed.mjs +1 -36
- package/src/splash.mjs +4 -0
- package/src/status.mjs +2 -71
- package/assets/APPEND-SYSTEM.md +0 -9
- package/assets/extensions/naraya-brand.ts +0 -251
- package/assets/extensions/naraya-gate.ts +0 -23
- package/assets/naraya-logo.txt +0 -5
- package/assets/skills/narabuild/SKILL.md +0 -156
- package/assets/skills/naradroid/SKILL.md +0 -118
- package/assets/skills/naraexplore/SKILL.md +0 -71
- package/assets/skills/narafe/SKILL.md +0 -94
- package/assets/skills/naraplan/SKILL.md +0 -47
- package/assets/skills/narasearch/SKILL.md +0 -141
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import type { McpServerConfig } from "./config-loader.js";
|
|
4
|
+
|
|
5
|
+
export async function createTransport(
|
|
6
|
+
config: McpServerConfig,
|
|
7
|
+
projectDir: string
|
|
8
|
+
): Promise<any> {
|
|
9
|
+
if (config.type === "http" || config.type === "streamable-http") {
|
|
10
|
+
if (!config.url) {
|
|
11
|
+
throw new Error("url is required for HTTP transport");
|
|
12
|
+
}
|
|
13
|
+
return new StreamableHTTPClientTransport(new URL(config.url), {
|
|
14
|
+
requestInit: {
|
|
15
|
+
headers: config.headers
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Default: stdio
|
|
21
|
+
if (!config.command) {
|
|
22
|
+
throw new Error("command is required for stdio transport");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const env: Record<string, string> = {
|
|
26
|
+
...process.env,
|
|
27
|
+
...config.env,
|
|
28
|
+
NARAYA_PROJECT_DIR: projectDir
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return new StdioClientTransport({
|
|
32
|
+
command: config.command,
|
|
33
|
+
args: config.args ?? [],
|
|
34
|
+
env,
|
|
35
|
+
// Suppress MCP server startup logs by discarding stderr
|
|
36
|
+
stderr: "ignore"
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/mcp-cli.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import p from"node:fs";import l from"node:path";import g from"node:os";const f=l.join(g.homedir(),".naraya","agent"),d=l.join(f,"mcp.json");function a(){try{return JSON.parse(p.readFileSync(d,"utf8"))}catch{return{mcpServers:{}}}}function m(e){p.mkdirSync(f,{recursive:!0}),p.writeFileSync(d,JSON.stringify(e,null,2))}function y(){const e=a();e.mcpServers=e.mcpServers||{},e.mcpServers.context7||(e.mcpServers.context7={command:"npx",args:["-y","@upstash/context7-mcp@latest"]}),m(e)}async function x(e){const s=e[3];if(s==="list"||!s){const o=a(),r=Object.keys(o.mcpServers??{});if(r.length===0){console.log("No MCP servers configured.");return}console.log(`
|
|
2
|
+
Configured MCP servers:
|
|
3
|
+
`);for(const n of r){const t=o.mcpServers[n],c=t.type==="http"||t.type==="streamable-http"?"HTTP":"stdio";console.log(` ${n} (${c})`)}console.log();return}if(s==="add"){const o=e[4];o||(console.error("Usage: naraya mcp add <name> [--transport http <url>] -- <command> [args...]"),process.exit(1));const r=a(),n=e.indexOf("--",5),t=e.indexOf("--transport",5);if(t!==-1&&n===-1){const c=e[t+1]==="http"?"http":"streamable-http",i=e[t+2];i||(console.error("Usage: naraya mcp add <name> --transport http <url>"),process.exit(1)),r.mcpServers[o]={type:c,url:i}}else if(n!==-1){const c=e[n+1],i=e.slice(n+2);c||(console.error("Usage: naraya mcp add <name> -- <command> [args...]"),process.exit(1)),r.mcpServers[o]={command:c,args:i}}else console.error("Usage: naraya mcp add <name> [--transport http <url>] -- <command> [args...]"),process.exit(1);m(r),console.log(`Added MCP server: ${o}`);return}if(s==="remove"){const o=e[4];o||(console.error("Usage: naraya mcp remove <name>"),process.exit(1));const r=a();r.mcpServers[o]||(console.error(`Server '${o}' not found.`),process.exit(1)),delete r.mcpServers[o],m(r),console.log(`Removed MCP server: ${o}`);return}if(s==="get"){const o=e[4];o||(console.error("Usage: naraya mcp get <name>"),process.exit(1));const n=a().mcpServers[o];n||(console.error(`Server '${o}' not found.`),process.exit(1)),console.log(`
|
|
4
|
+
${o}:
|
|
5
|
+
`),console.log(JSON.stringify(n,null,2)),console.log();return}console.error(`Unknown action: ${s}`),console.error("Available: list, add, remove, get"),process.exit(1)}export{y as ensureBundledMcpServers,x as mcpCLI};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ToolsCatalog, ToolEntry } from "../types.js"
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CATALOG_PATH = "tools-catalog.json"
|
|
4
|
+
|
|
5
|
+
export interface CatalogLoaderOptions {
|
|
6
|
+
readonly catalogPath?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function loadToolsCatalog(options?: CatalogLoaderOptions): Promise<ToolsCatalog> {
|
|
10
|
+
const path = options?.catalogPath ?? DEFAULT_CATALOG_PATH
|
|
11
|
+
const response = await fetch(`file://${process.cwd()}/${path}`)
|
|
12
|
+
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new Error(`Failed to load tools catalog from ${path}: ${response.statusText}`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return response.json() as Promise<ToolsCatalog>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function loadToolsCatalogFromFs(fs: typeof import("fs/promises"), options?: CatalogLoaderOptions): Promise<ToolsCatalog> {
|
|
21
|
+
const path = options?.catalogPath ?? DEFAULT_CATALOG_PATH
|
|
22
|
+
const content = await fs.readFile(path, "utf-8")
|
|
23
|
+
return JSON.parse(content) as ToolsCatalog
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getToolByName(catalog: ToolsCatalog, name: string): ToolEntry | undefined {
|
|
27
|
+
return catalog.tools.find((t: ToolEntry) => t.tools_name === name)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getToolNames(catalog: ToolsCatalog): string[] {
|
|
31
|
+
return catalog.tools.map((t: ToolEntry) => t.tools_name)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function validateCatalog(catalog: unknown): catalog is ToolsCatalog {
|
|
35
|
+
if (typeof catalog !== "object" || catalog === null) return false
|
|
36
|
+
const c = catalog as Record<string, unknown>
|
|
37
|
+
if (typeof c.$schema !== "string") return false
|
|
38
|
+
if (typeof c.version !== "string") return false
|
|
39
|
+
if (!Array.isArray(c.categories)) return false
|
|
40
|
+
if (!Array.isArray(c.tools)) return false
|
|
41
|
+
return c.tools.every(validateToolEntry)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateToolEntry(tool: unknown): tool is ToolEntry {
|
|
45
|
+
if (typeof tool !== "object" || tool === null) return false
|
|
46
|
+
const t = tool as Record<string, unknown>
|
|
47
|
+
return (
|
|
48
|
+
typeof t.tools_name === "string" &&
|
|
49
|
+
typeof t.description === "string" &&
|
|
50
|
+
typeof t.category === "string" &&
|
|
51
|
+
typeof t.command === "object" &&
|
|
52
|
+
typeof t.skills_loader === "string" &&
|
|
53
|
+
Array.isArray(t.phase)
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./catalog-loader.js"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { PentestManager } from "./manager.js"
|
|
2
|
+
import type { PentestPhase, SkillLoadResult, PentestSkillManifest } from "./types.js"
|
|
3
|
+
import { Command } from "commander"
|
|
4
|
+
|
|
5
|
+
export async function pentestCLI(argv: string[]) {
|
|
6
|
+
const program = new Command()
|
|
7
|
+
.name("naraya pentest")
|
|
8
|
+
.description("Pentest orchestration for authorized engagements")
|
|
9
|
+
.version("0.2.0")
|
|
10
|
+
|
|
11
|
+
const manager = new PentestManager()
|
|
12
|
+
|
|
13
|
+
// naraya pentest list
|
|
14
|
+
program
|
|
15
|
+
.command("list")
|
|
16
|
+
.description("List all available pentest skills")
|
|
17
|
+
.action(() => {
|
|
18
|
+
const manifests = manager.discoverSkills()
|
|
19
|
+
|
|
20
|
+
if (manifests.length === 0) {
|
|
21
|
+
console.log("No pentest skills found.")
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`Found ${manifests.length} skill(s):\n`)
|
|
26
|
+
for (const m of manifests) {
|
|
27
|
+
console.log(` * ${m.name} v${m.version} [${m.phase.join(", ")}]`)
|
|
28
|
+
console.log(` ${m.description}`)
|
|
29
|
+
console.log(` Tools: ${m.tools.join(", ")}`)
|
|
30
|
+
console.log(` Tags: ${m.tags.join(", ")}\n`)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// naraya pentest mode --target example.com
|
|
35
|
+
program
|
|
36
|
+
.command("mode")
|
|
37
|
+
.description("Auto-detect or set pentest mode")
|
|
38
|
+
.option("--target <target>", "Target to analyze for mode detection")
|
|
39
|
+
.option("--mode <mode>", "Force specific mode (auto|ctf|bug-bounty|red-team|blue-team|offensive|grey-hat)")
|
|
40
|
+
.action((options) => {
|
|
41
|
+
const result = manager.selectMode({
|
|
42
|
+
target: options.target,
|
|
43
|
+
preferred_mode: options.mode,
|
|
44
|
+
auto_detect: options.target !== undefined,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
console.log(`\nMode: ${result.selected_mode}`)
|
|
48
|
+
console.log(`Auto-detected: ${result.auto_detected ? "Yes" : "No"}`)
|
|
49
|
+
console.log(`Reason: ${result.detection_reason}`)
|
|
50
|
+
console.log(`\nConfig:`)
|
|
51
|
+
console.log(` Description: ${result.config.description}`)
|
|
52
|
+
console.log(` Parallelism: ${result.config.parallelism}`)
|
|
53
|
+
console.log(` Stealth: ${result.config.stealth ? "Yes" : "No"}`)
|
|
54
|
+
console.log(` Report Format: ${result.config.report_format}`)
|
|
55
|
+
console.log(` Skill Chain: ${result.config.skill_chain.join(" -> ")}`)
|
|
56
|
+
console.log(` Tool Priority: ${result.config.tool_priority.join(" -> ")}\n`)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// naraya pentest phase --phase recon
|
|
60
|
+
program
|
|
61
|
+
.command("phase")
|
|
62
|
+
.description("Run a pentest phase")
|
|
63
|
+
.option("--target <url>", "Target domain or IP")
|
|
64
|
+
.option("--phase <phase>", "Phase: recon, enumeration, exploitation, reporting")
|
|
65
|
+
.option("--mode <mode>", "Pentest mode")
|
|
66
|
+
.option("--passive", "Passive-only mode")
|
|
67
|
+
.action((options) => {
|
|
68
|
+
if (!options.target) {
|
|
69
|
+
console.error("Error: --target is required")
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
72
|
+
if (!options.phase) {
|
|
73
|
+
console.error("Error: --phase is required")
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const mode = options.mode || "auto"
|
|
78
|
+
const modeConfig = manager.getModeConfig(mode)
|
|
79
|
+
|
|
80
|
+
console.log(`Running ${options.phase} on ${options.target}...`)
|
|
81
|
+
console.log(`Mode: ${mode}`)
|
|
82
|
+
console.log(`Stealth: ${modeConfig.stealth ? "Yes" : "No"}`)
|
|
83
|
+
console.log(`Parallelism: ${modeConfig.parallelism}`)
|
|
84
|
+
console.log(`Skills: ${modeConfig.skill_chain.join(" -> ")}`)
|
|
85
|
+
|
|
86
|
+
const skills = manager.loadSkillsByPhase(options.phase)
|
|
87
|
+
const loaded = skills.filter((s: SkillLoadResult) => s.loaded)
|
|
88
|
+
if (loaded.length > 0) {
|
|
89
|
+
console.log(`\nLoaded ${loaded.length} skill(s):`)
|
|
90
|
+
for (const s of loaded) {
|
|
91
|
+
console.log(` * ${s.name} v${s.skill?.version}`)
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
console.log(`\nNo skills found for phase: ${options.phase}`)
|
|
95
|
+
}
|
|
96
|
+
console.log(`\n${options.phase} completed.`)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// naraya pentest register
|
|
100
|
+
program
|
|
101
|
+
.command("register")
|
|
102
|
+
.description("Register skills with the skill register")
|
|
103
|
+
.option("--skill <name>", "Register specific skill")
|
|
104
|
+
.option("--all", "Register all discovered skills")
|
|
105
|
+
.action((options) => {
|
|
106
|
+
const register = manager.getRegister()
|
|
107
|
+
|
|
108
|
+
if (options.all) {
|
|
109
|
+
const manifests = manager.discoverSkills()
|
|
110
|
+
const entries = register.registerFromFiles(manifests.map((m: PentestSkillManifest) => m.name))
|
|
111
|
+
console.log(`Registered ${entries.length} skill(s)`)
|
|
112
|
+
} else if (options.skill) {
|
|
113
|
+
const entry = register.registerFromFile(options.skill)
|
|
114
|
+
if (entry) {
|
|
115
|
+
console.log(`Registered: ${entry.name} v${entry.skill.version}`)
|
|
116
|
+
} else {
|
|
117
|
+
console.error(`Skill not found: ${options.skill}`)
|
|
118
|
+
process.exit(1)
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
console.log("Use --skill <name> or --all to register skills")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(`\nTotal registered: ${register.count()}`)
|
|
125
|
+
console.log(`Enabled: ${register.enabledCount()}`)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
program.parse(argv)
|
|
129
|
+
}
|
|
130
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ToolEntry, CommandBuildOptions, CommandBuildResult, FlagDefinition } from "../types.js"
|
|
2
|
+
|
|
3
|
+
export function buildCommand(tool: ToolEntry, options: CommandBuildOptions = {}): CommandBuildResult {
|
|
4
|
+
const args: string[] = []
|
|
5
|
+
const flags = options.flags ?? {}
|
|
6
|
+
const positional = options.positional ?? []
|
|
7
|
+
|
|
8
|
+
for (const flagDef of tool.command.flags) {
|
|
9
|
+
const value = flags[flagDef.name]
|
|
10
|
+
const argValue = buildFlagArg(flagDef, value)
|
|
11
|
+
if (argValue) {
|
|
12
|
+
args.push(...argValue)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (positional.length > 0) {
|
|
17
|
+
args.push(...positional)
|
|
18
|
+
} else {
|
|
19
|
+
for (const posDef of tool.command.positional) {
|
|
20
|
+
const value = flags[posDef.name]
|
|
21
|
+
if (value !== undefined) {
|
|
22
|
+
args.push(String(value))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let fullCommand = `${tool.command.base} ${args.join(" ")}`.trim()
|
|
28
|
+
|
|
29
|
+
if (options.pipe_to && options.pipe_to.length > 0) {
|
|
30
|
+
fullCommand += ` | ${options.pipe_to.join(" | ")}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
command: tool.command.base,
|
|
35
|
+
args,
|
|
36
|
+
fullCommand,
|
|
37
|
+
requires_root: tool.requires_root
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildFlagArg(flag: FlagDefinition, value: unknown): string[] | null {
|
|
42
|
+
if (value === undefined || value === null) {
|
|
43
|
+
if (flag.default !== undefined && flag.type === "boolean" && flag.default === true) {
|
|
44
|
+
return [flag.name]
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
switch (flag.type) {
|
|
50
|
+
case "boolean":
|
|
51
|
+
return value === true ? [flag.name] : null
|
|
52
|
+
|
|
53
|
+
case "string":
|
|
54
|
+
case "number":
|
|
55
|
+
return [flag.name, String(value)]
|
|
56
|
+
|
|
57
|
+
case "path":
|
|
58
|
+
return [flag.name, String(value)]
|
|
59
|
+
|
|
60
|
+
case "choice":
|
|
61
|
+
if (flag.choices && !flag.choices.includes(String(value))) {
|
|
62
|
+
throw new Error(`Invalid choice for ${flag.name}: ${value}. Valid: ${flag.choices.join(", ")}`)
|
|
63
|
+
}
|
|
64
|
+
return [flag.name, String(value)]
|
|
65
|
+
|
|
66
|
+
case "repeat":
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
return value.flatMap(v => [flag.name, String(v)])
|
|
69
|
+
}
|
|
70
|
+
return [flag.name, String(value)]
|
|
71
|
+
|
|
72
|
+
default:
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildCommandWithSudo(tool: ToolEntry, options: CommandBuildOptions = {}): string {
|
|
78
|
+
const result = buildCommand(tool, options)
|
|
79
|
+
return result.requires_root ? `sudo ${result.fullCommand}` : result.fullCommand
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function buildPipeline(tools: ToolEntry[], optionsPerTool: CommandBuildOptions[]): string {
|
|
83
|
+
const commands = tools.map((tool, i) => buildCommand(tool, optionsPerTool[i]))
|
|
84
|
+
return commands.map(c => c.fullCommand).join(" | ")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function validateRequiredFlags(tool: ToolEntry, options: CommandBuildOptions): string[] {
|
|
88
|
+
const errors: string[] = []
|
|
89
|
+
const flags = options.flags ?? {}
|
|
90
|
+
|
|
91
|
+
for (const flag of tool.command.flags) {
|
|
92
|
+
if (flag.required && flags[flag.name] === undefined) {
|
|
93
|
+
errors.push(`Missing required flag: ${flag.name}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const pos of tool.command.positional) {
|
|
98
|
+
if (pos.required) {
|
|
99
|
+
const hasValue = options.positional?.length
|
|
100
|
+
? options.positional.length > 0
|
|
101
|
+
: flags[pos.name] !== undefined
|
|
102
|
+
if (!hasValue) {
|
|
103
|
+
errors.push(`Missing required positional argument: ${pos.name}`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return errors
|
|
109
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./command-builder.js"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./catalog/index.js"
|
|
2
|
+
export * from "./selector/index.js"
|
|
3
|
+
export * from "./command-builder/index.js"
|
|
4
|
+
export * from "./installer/index.js"
|
|
5
|
+
export * from "./skill-bridge/index.js"
|
|
6
|
+
export * from "./mode/index.js"
|
|
7
|
+
// skills re-exports overlap with types - only re-export the classes/functions, not types
|
|
8
|
+
export { discoverSkills, loadSkill, loadAllSkills, loadSkillsByPhase, loadSkillsByTools, getSkillsForTool, resolveSkillPath as resolveSkillPathFromLoader } from "./skills/loader/skill-loader.js"
|
|
9
|
+
export { SkillRegister, createSkillRegister } from "./skills/register/skill-register.js"
|
|
10
|
+
export { generateSkill, generateAndSaveSkill, generateSkillsForPhase, generateSkillsForTool, generateFullPentestSuite, saveGeneratedSkills } from "./skills/generator/skill-generator.js"
|
|
11
|
+
export type * from "./types.js"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./tool-installer.js"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ToolEntry, ToolAvailability, InstallationConfig, PlatformInstallation } from "../types.js"
|
|
2
|
+
import { exec } from "child_process"
|
|
3
|
+
import { promisify } from "util"
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec)
|
|
6
|
+
|
|
7
|
+
export async function checkToolInstalled(tool: ToolEntry): Promise<ToolAvailability> {
|
|
8
|
+
try {
|
|
9
|
+
const { stdout } = await execAsync(tool.check_installed.command, { timeout: 10000 })
|
|
10
|
+
|
|
11
|
+
let version: string | undefined
|
|
12
|
+
if (tool.check_installed.parse_version) {
|
|
13
|
+
const match = stdout.match(new RegExp(tool.check_installed.parse_version))
|
|
14
|
+
version = match?.[1]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
tools_name: tool.tools_name,
|
|
19
|
+
installed: true,
|
|
20
|
+
version
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return {
|
|
24
|
+
tools_name: tool.tools_name,
|
|
25
|
+
installed: false,
|
|
26
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function checkAllToolsInstalled(tools: readonly ToolEntry[]): Promise<ToolAvailability[]> {
|
|
32
|
+
return Promise.all(tools.map(checkToolInstalled))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getInstallCommand(tool: ToolEntry, platform: NodeJS.Platform = process.platform): string | null {
|
|
36
|
+
const config = tool.installation[platform as keyof InstallationConfig]
|
|
37
|
+
return config?.command ?? null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getInstallCommands(tool: ToolEntry): Partial<Record<NodeJS.Platform, PlatformInstallation>> {
|
|
41
|
+
const result: Partial<Record<NodeJS.Platform, PlatformInstallation>> = {}
|
|
42
|
+
|
|
43
|
+
if (tool.installation.linux) result.linux = tool.installation.linux
|
|
44
|
+
if (tool.installation.darwin) result.darwin = tool.installation.darwin
|
|
45
|
+
if (tool.installation.win32) result.win32 = tool.installation.win32
|
|
46
|
+
|
|
47
|
+
return result
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function installTool(tool: ToolEntry, platform: NodeJS.Platform = process.platform): Promise<{ success: boolean; message: string }> {
|
|
51
|
+
const config = tool.installation[platform as keyof InstallationConfig]
|
|
52
|
+
|
|
53
|
+
if (!config) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
message: `No installation command available for platform: ${platform}`
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const { stdout, stderr } = await execAsync(config.command, { timeout: 300000 })
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
message: stdout || stderr || "Installation completed"
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
message: error instanceof Error ? error.message : "Installation failed"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getMissingTools(availability: ToolAvailability[]): ToolAvailability[] {
|
|
75
|
+
return availability.filter(a => !a.installed)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getInstalledTools(availability: ToolAvailability[]): ToolAvailability[] {
|
|
79
|
+
return availability.filter(a => a.installed)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function ensureToolsInstalled(tools: readonly ToolEntry[], platform: NodeJS.Platform = process.platform): Promise<{
|
|
83
|
+
installed: ToolAvailability[]
|
|
84
|
+
missing: ToolAvailability[]
|
|
85
|
+
failed: { tool: string; error: string }[]
|
|
86
|
+
}> {
|
|
87
|
+
const availability = await checkAllToolsInstalled(tools)
|
|
88
|
+
const missing = getMissingTools(availability)
|
|
89
|
+
const failed: { tool: string; error: string }[] = []
|
|
90
|
+
|
|
91
|
+
for (const tool of missing) {
|
|
92
|
+
const toolEntry = tools.find(t => t.tools_name === tool.tools_name)
|
|
93
|
+
if (!toolEntry) continue
|
|
94
|
+
|
|
95
|
+
const result = await installTool(toolEntry, platform)
|
|
96
|
+
if (!result.success) {
|
|
97
|
+
failed.push({ tool: tool.tools_name, error: result.message })
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const recheck = await checkAllToolsInstalled(tools)
|
|
102
|
+
return {
|
|
103
|
+
installed: getInstalledTools(recheck),
|
|
104
|
+
missing: getMissingTools(recheck),
|
|
105
|
+
failed
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { PentestSkill, PentestSkillManifest, SkillLoadResult, ToolsCatalog, ToolEntry, ToolSelectorOptions, PentestPhase, PentestMode, ModeConfig, ModeSelectorOptions, ModeSelectionResult, ModeLoopConfig, SafetyConfig } from "./types.js"
|
|
2
|
+
import { MODE_PRESETS } from "./types.js"
|
|
3
|
+
import { discoverSkills, loadSkill, loadAllSkills, loadSkillsByPhase } from "./skills/loader/skill-loader.js"
|
|
4
|
+
import { SkillRegister } from "./skills/register/skill-register.js"
|
|
5
|
+
import { selectTools, selectToolsByPhase, selectToolsByCategory, groupToolsByPhase, groupToolsByCategory } from "./selector/tool-selector.js"
|
|
6
|
+
import { buildCommand, buildCommandWithSudo, validateRequiredFlags } from "./command-builder/command-builder.js"
|
|
7
|
+
import { checkToolInstalled, checkAllToolsInstalled, getInstallCommand, installTool } from "./installer/tool-installer.js"
|
|
8
|
+
import { selectMode, detectMode, getModeConfig, getToolsForMode, getSkillsForMode, getLoopConfig, getSafetyConfig } from "./mode/mode-selector.js"
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs"
|
|
10
|
+
import { join, resolve } from "path"
|
|
11
|
+
|
|
12
|
+
export interface PentestManagerConfig {
|
|
13
|
+
baseDir?: string
|
|
14
|
+
skillDirs?: string[]
|
|
15
|
+
configPath?: string
|
|
16
|
+
catalogPath?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_SKILL_DIRS = [
|
|
20
|
+
"assets/agents/skills",
|
|
21
|
+
"skills/",
|
|
22
|
+
"packages/pentest-skills/skills",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const DEFAULT_CATALOG_PATH = "tools-catalog.json"
|
|
26
|
+
|
|
27
|
+
export class PentestManager {
|
|
28
|
+
private readonly config: Required<PentestManagerConfig>
|
|
29
|
+
private readonly cache = {
|
|
30
|
+
manifests: new Map<string, PentestSkillManifest[]>(),
|
|
31
|
+
skills: new Map<string, SkillLoadResult>(),
|
|
32
|
+
catalog: null as ToolsCatalog | null,
|
|
33
|
+
}
|
|
34
|
+
private skillRegister: SkillRegister | null = null
|
|
35
|
+
|
|
36
|
+
constructor(config: PentestManagerConfig = {}) {
|
|
37
|
+
this.config = {
|
|
38
|
+
baseDir: config.baseDir ?? process.cwd(),
|
|
39
|
+
skillDirs: config.skillDirs ?? DEFAULT_SKILL_DIRS,
|
|
40
|
+
configPath: config.configPath ?? join(config.baseDir ?? process.cwd(), ".pentestrc"),
|
|
41
|
+
catalogPath: config.catalogPath ?? join(config.baseDir ?? process.cwd(), DEFAULT_CATALOG_PATH),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Mode Operations ──
|
|
46
|
+
|
|
47
|
+
selectMode(options: ModeSelectorOptions = {}): ModeSelectionResult {
|
|
48
|
+
return selectMode(options)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
detectMode(target: string): { mode: PentestMode; reason: string } {
|
|
52
|
+
return detectMode(target)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getModeConfig(mode: PentestMode): ModeConfig {
|
|
56
|
+
return getModeConfig(mode)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getSkillsForMode(mode: PentestMode): string[] {
|
|
60
|
+
return getSkillsForMode(mode)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getLoopConfig(mode: PentestMode): ModeLoopConfig {
|
|
64
|
+
return getLoopConfig(mode)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getSafetyConfig(mode: PentestMode): SafetyConfig {
|
|
68
|
+
return getSafetyConfig(mode)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Skill Operations ──
|
|
72
|
+
|
|
73
|
+
discoverSkills(): PentestSkillManifest[] {
|
|
74
|
+
return discoverSkills(this.config.baseDir)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
loadSkill(skillName: string): SkillLoadResult {
|
|
78
|
+
return loadSkill(skillName, this.config.baseDir)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
loadAllSkills(): SkillLoadResult[] {
|
|
82
|
+
return loadAllSkills(this.config.baseDir)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
loadSkillsByPhase(phase: string): SkillLoadResult[] {
|
|
86
|
+
return loadSkillsByPhase(phase, this.config.baseDir)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Catalog Operations ──
|
|
90
|
+
|
|
91
|
+
async loadCatalog(): Promise<ToolsCatalog> {
|
|
92
|
+
if (this.cache.catalog) return this.cache.catalog
|
|
93
|
+
|
|
94
|
+
const catalogPath = this.config.catalogPath
|
|
95
|
+
if (!existsSync(catalogPath)) {
|
|
96
|
+
throw new Error(`Tools catalog not found: ${catalogPath}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const content = readFileSync(catalogPath, "utf-8")
|
|
100
|
+
this.cache.catalog = JSON.parse(content) as ToolsCatalog
|
|
101
|
+
return this.cache.catalog
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async selectTools(options: ToolSelectorOptions): Promise<ToolEntry[]> {
|
|
105
|
+
const catalog = await this.loadCatalog()
|
|
106
|
+
return selectTools(catalog, options)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async selectToolsByPhase(phase: PentestPhase): Promise<ToolEntry[]> {
|
|
110
|
+
const catalog = await this.loadCatalog()
|
|
111
|
+
return selectToolsByPhase(catalog, phase)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async selectToolsByCategory(category: "recon" | "enumeration" | "exploitation" | "reporting" | "utility"): Promise<ToolEntry[]> {
|
|
115
|
+
const catalog = await this.loadCatalog()
|
|
116
|
+
return selectToolsByCategory(catalog, category)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Command Operations ──
|
|
120
|
+
|
|
121
|
+
buildToolCommand(tool: ToolEntry, flags: Record<string, unknown> = {}, positional: string[] = []): string {
|
|
122
|
+
const result = buildCommand(tool, { flags, positional })
|
|
123
|
+
return result.fullCommand
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
buildToolCommandWithSudo(tool: ToolEntry, flags: Record<string, unknown> = {}, positional: string[] = []): string {
|
|
127
|
+
return buildCommandWithSudo(tool, { flags, positional })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
validateToolFlags(tool: ToolEntry, flags: Record<string, unknown>): string[] {
|
|
131
|
+
return validateRequiredFlags(tool, { flags })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Installation Operations ──
|
|
135
|
+
|
|
136
|
+
async checkToolInstalled(tool: ToolEntry) {
|
|
137
|
+
return checkToolInstalled(tool)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async checkAllToolsInstalled(tools: readonly ToolEntry[]) {
|
|
141
|
+
return checkAllToolsInstalled(tools)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getInstallCommandForTool(tool: ToolEntry): string | null {
|
|
145
|
+
return getInstallCommand(tool)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async installTool(tool: ToolEntry) {
|
|
149
|
+
return installTool(tool)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Skill Register ──
|
|
153
|
+
|
|
154
|
+
getRegister(): SkillRegister {
|
|
155
|
+
if (!this.skillRegister) {
|
|
156
|
+
this.skillRegister = new SkillRegister({
|
|
157
|
+
skills_dir: join(this.config.baseDir, ".agents/skills"),
|
|
158
|
+
search_paths: [
|
|
159
|
+
join(this.config.baseDir, ".agents/skills"),
|
|
160
|
+
join(this.config.baseDir, ".opencode/skills"),
|
|
161
|
+
join(this.config.baseDir, ".claude/skills"),
|
|
162
|
+
],
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
return this.skillRegister
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./mode-selector.js"
|