Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,48 @@ Use `/plugins` to list installed plugins.

---

## From candidates to CVE-grade evidence (Inquisitor)

KCode ships the full audit-and-fix pipeline as open source — discovery,
LLM verification, agentic re-verification, and agentic fix generation.
For most users that's enough: open a PR with a verified patch, done.

A small number of users need to go further: standalone reproducers,
binary scans that actually validate the finding, signed disclosure
bundles ready for a GitHub Security Advisory or a Bugcrowd submission.
That last mile is handled by [Inquisitor][inq], our paid sister
service. Inquisitor's daemon does the work behind a service boundary;
KCode talks to it over HTTP.

[inq]: https://astrolexis.space/inquisitor

| Command | Open / Paid | Action |
| ------------------ | :---------: | ----------------------------------------------- |
| `kcode audit` | Open | Discovery + LLM verifier → confirmed findings |
| `kcode --print …` | Open | Agentic re-verify and fix generation |
| `kcode reproduce` | **Inq.** | Standalone compilable reproducer (Mender) |
| `kcode validate` | **Inq.** | Adversarial-input binary scan (VulnHunter) |
| `kcode bundle` | **Inq.** | Signed disclosure bundle (GHSA / Bugcrowd / …) |
| `kcode disclose` | **Inq.** | Submit bundle to intake channel |

Free tier covers small-volume use (5 sessions / month). Paid tiers
(Starter / Pro / Team / Enterprise) unlock concurrency, external
intake submission, and signed bundles without watermark. Pricing,
signup, and self-service tokens: <https://astrolexis.space/inquisitor>.

Configuration:

| Env var | Default |
| -------------------------- | ---------------------------------------------------- |
| `INQUISITOR_URL` | `https://api.astrolexis.space/inquisitor/v1` |
| `INQUISITOR_TOKEN_FILE` | `~/.inquisitor/token` |
| `INQUISITOR_TOKEN` | (overrides the token file if set) |

Self-hosted Inquisitor (Enterprise tier): point `INQUISITOR_URL` at
your own deployment. KCode is endpoint-agnostic.

---

## Keyboard Shortcuts (TUI)

| Key | Action |
Expand Down
141 changes: 141 additions & 0 deletions src/cli/commands/bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// KCode — `kcode bundle` command
//
// Generate a signed disclosure bundle from a set of confirmed
// findings + their reproducers + (optionally) validation results.
// The output is a tarball with a signature file, suitable for
// attaching to a GitHub Security Advisory, a Bugcrowd submission,
// or a coordinated-disclosure email thread.

import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { resolve as pathResolve } from "node:path";
import type { Command } from "commander";
import {
formatInquisitorError,
InquisitorError,
requireInquisitor,
submitToInquisitor,
} from "../../integrations/inquisitor";

interface BundleResponse {
session_id: string;
bundle_b64: string;
bundle_filename: string;
signature: string;
format: string;
}

export function registerBundleCommand(program: Command): void {
program
.command("bundle <finding-ids...>")
.description("Generate a signed disclosure bundle from confirmed findings (Inquisitor)")
.option("-r, --report <path>", "audit report JSON to source findings from", "AUDIT_REPORT.json")
.option("-f, --format <fmt>", "output format: ghsa, bugcrowd, soc-email, oss-pr", "ghsa")
.option(
"-o, --out <path>",
"output file path (default: ./disclosure-bundle.tar.gz)",
"./disclosure-bundle.tar.gz",
)
.option(
"--include-repro <dir>",
"include a pre-built reproducer directory (from `kcode reproduce`)",
)
.option("--include-screencast <path>", "include an asciinema .mp4/.gif demo screencast")
.action(
async (
findingIds: string[],
opts: {
report: string;
format: string;
out: string;
includeRepro?: string;
includeScreencast?: string;
},
) => {
try {
const reportPath = pathResolve(opts.report);
if (!existsSync(reportPath)) {
console.error(`✗ Audit report not found: ${reportPath}`);
process.exit(2);
}
const report = JSON.parse(readFileSync(reportPath, "utf8")) as {
confirmed?: Array<Record<string, unknown>>;
};
const wanted = new Set(findingIds);
const findings = (report.confirmed ?? []).filter((f) =>
wanted.has(String(f.id ?? f.finding_id ?? "")),
);
if (findings.length === 0) {
console.error(`✗ None of the requested findings were in the report`);
process.exit(2);
}
if (findings.length !== findingIds.length) {
const found = new Set(findings.map((f) => String(f.id ?? f.finding_id ?? "")));
const missing = findingIds.filter((id) => !found.has(id));
console.error(`! Missing findings (skipped): ${missing.join(", ")}`);
}

const repro = opts.includeRepro
? readReproDir(pathResolve(opts.includeRepro))
: undefined;
const screencast = opts.includeScreencast
? readBase64(pathResolve(opts.includeScreencast))
: undefined;

const pre = await requireInquisitor("bundle");
console.log(
`→ Inquisitor preflight ok (tier=${pre.tier}, balance=${pre.balance}). Generating ${opts.format} bundle for ${findings.length} finding(s)…`,
);

const result = await submitToInquisitor<BundleResponse>("bundle", {
findings,
format: opts.format,
repro,
screencast,
});

const outPath = pathResolve(opts.out);
writeFileSync(outPath, Buffer.from(result.bundle_b64, "base64"));
writeFileSync(`${outPath}.sig`, result.signature);
console.log(`\n✓ Bundle: ${outPath}`);
console.log(` Signature: ${outPath}.sig`);
console.log(` Format: ${result.format}`);
console.log(`\n Verify: inquisitor verify ${outPath}`);
} catch (err) {
if (err instanceof InquisitorError) {
console.error(formatInquisitorError(err));
process.exit(1);
}
throw err;
}
},
);
}

function readReproDir(
dir: string,
): { manifest: unknown; files: Array<{ path: string; content_b64: string }> } | undefined {
if (!existsSync(dir)) return undefined;
// Minimal: caller passes a directory; Inquisitor will tar+gzip it
// on its side. We just hand over the file list + contents as base64
// so the bridge stays JSON.
const { readdirSync, statSync } = require("node:fs") as typeof import("node:fs");
const out: Array<{ path: string; content_b64: string }> = [];
const walk = (root: string, prefix: string) => {
for (const name of readdirSync(root)) {
const p = `${root}/${name}`;
const rel = prefix ? `${prefix}/${name}` : name;
const st = statSync(p);
if (st.isDirectory()) walk(p, rel);
else if (st.isFile()) {
out.push({ path: rel, content_b64: readFileSync(p).toString("base64") });
}
}
};
walk(dir, "");
return { manifest: { source: dir, file_count: out.length }, files: out };
}

function readBase64(p: string): string | undefined {
if (!existsSync(p)) return undefined;
return readFileSync(p).toString("base64");
}
90 changes: 90 additions & 0 deletions src/cli/commands/disclose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// KCode — `kcode disclose` command
//
// Submit a previously-generated disclosure bundle to an intake
// channel: GitHub Security Advisory (PVR), Bugcrowd VDP, vendor
// SOC email, or an OSS hygiene PR.
//
// This step is the last mile of the audit → disclosure pipeline.
// It is gated behind Inquisitor because (a) the intake adapters
// are non-trivial to maintain, (b) some intakes require signed
// bundles, and (c) audit-trail / receipt persistence lives on
// the Inquisitor side.

import { existsSync, readFileSync } from "node:fs";
import { resolve as pathResolve } from "node:path";
import type { Command } from "commander";
import {
formatInquisitorError,
InquisitorError,
requireInquisitor,
submitToInquisitor,
} from "../../integrations/inquisitor";

interface DiscloseResponse {
session_id: string;
submission_id: string;
intake: string;
intake_url: string;
state: "submitted" | "queued" | "rejected";
message?: string;
}

export function registerDiscloseCommand(program: Command): void {
program
.command("disclose <bundle-path>")
.description("Submit a signed disclosure bundle to an intake channel (Inquisitor)")
.requiredOption(
"-i, --intake <target>",
"intake spec — examples:\n" +
" ghsa:owner/repo (GitHub Security Advisory, requires PVR enabled)\n" +
" bugcrowd:engagement-slug (Bugcrowd VDP / bounty program)\n" +
" email:security@vendor.com (vendor SOC email with bundle attached)\n" +
" pr:owner/repo (open a public PR with hygiene patches only)",
)
.option("--dry-run", "validate the bundle + intake spec without actually submitting", false)
.action(async (bundlePath: string, opts: { intake: string; dryRun: boolean }) => {
try {
const p = pathResolve(bundlePath);
if (!existsSync(p)) {
console.error(`✗ Bundle not found: ${p}`);
console.error(" Generate one first with: kcode bundle <finding-ids…>");
process.exit(2);
}
const sigPath = `${p}.sig`;
if (!existsSync(sigPath)) {
console.error(`✗ Bundle signature not found: ${sigPath}`);
console.error(" Re-run `kcode bundle` to produce a signed bundle.");
process.exit(2);
}
const bundle_b64 = readFileSync(p).toString("base64");
const signature = readFileSync(sigPath, "utf8").trim();

const pre = await requireInquisitor("disclose");
console.log(`→ Inquisitor preflight ok (tier=${pre.tier}, balance=${pre.balance}).`);
console.log(` ${opts.dryRun ? "Dry-run validation" : "Submitting"} to ${opts.intake}…`);

const result = await submitToInquisitor<DiscloseResponse>("disclose", {
bundle_b64,
signature,
intake: opts.intake,
dry_run: opts.dryRun,
});

if (opts.dryRun) {
console.log(`\n✓ Dry-run ok — intake '${result.intake}' would accept the bundle.`);
return;
}
console.log(`\n✓ Submitted: ${result.submission_id}`);
console.log(` State: ${result.state}`);
console.log(` Intake: ${result.intake}`);
console.log(` URL: ${result.intake_url}`);
if (result.message) console.log(` Message: ${result.message}`);
} catch (err) {
if (err instanceof InquisitorError) {
console.error(formatInquisitorError(err));
process.exit(1);
}
throw err;
}
});
}
4 changes: 4 additions & 0 deletions src/cli/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ export { registerAneCommand } from "./ane";
export { registerAuditCommand } from "./audit";
export { registerAuthCommand } from "./auth";
export { registerBenchmarkCommands } from "./benchmark";
export { registerBundleCommand } from "./bundle";
export { registerCloudCommand } from "./cloud";
export { registerCompletionsCommand } from "./completions";
export { registerDaemonCommand } from "./daemon";
export { registerDashboardCommand } from "./dashboard";
export { registerDiscloseCommand } from "./disclose";
export { registerDistillCommand } from "./distill";
export { registerDoctorCommand } from "./doctor";
export { registerGrammarsCommand } from "./grammars";
Expand All @@ -21,6 +23,7 @@ export { registerPluginCommand } from "./plugin";
export { registerProCommands } from "./pro";
export { registerRagCommand } from "./rag";
export { registerRemoteCommand } from "./remote";
export { registerReproduceCommand } from "./reproduce";
export { registerResumeCommand } from "./resume";
export { registerSbomCommand } from "./sbom";
export { registerSearchCommand } from "./search";
Expand All @@ -33,5 +36,6 @@ export { registerTeachCommand } from "./teach";
export { registerTemplateCommand } from "./template";
export { registerTriggersCommand } from "./triggers";
export { registerUpdateCommand } from "./update";
export { registerValidateCommand } from "./validate";
export { registerWatchCommand } from "./watch";
export { registerWebCommand } from "./web";
Loading
Loading