@zivis/cli 0.1.0-alpha.44 → 0.1.0-alpha.45

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.
@@ -6,16 +6,17 @@ import { resolveConfigFromBinding } from "@zivis/mcp/project-binding";
6
6
  import { requireApplicationId } from "@zivis/mcp/resolve-application-id";
7
7
  import { renderSecurityContext } from "../../internal/security-context.js";
8
8
  import { startTestRun, listTestScopes } from "../../internal/test-scope.js";
9
- import { HttpPackRegistryClient, LocalPackRegistryClient, registryDirOverride } from "../../internal/packs/registry-client.js";
9
+ import { LocalPackRegistryClient, registryDirOverride } from "../../internal/packs/registry-client.js";
10
+ import { NpmPackRegistryClient } from "../../internal/packs/npm-registry-client.js";
10
11
  import { PackUnavailableError, PackIntegrityError, UnknownScopeError } from "../../internal/packs/types.js";
11
12
  import { jsonEnvelope, printJson, EXIT, exitCodeForError, errorMessage, failCommand } from "../../internal/cli-output.js";
12
13
  class UsageError extends Error {
13
14
  }
14
- function buildRegistryClient(apiClient) {
15
+ function buildRegistryClient() {
15
16
  const override = registryDirOverride();
16
17
  if (override)
17
18
  return new LocalPackRegistryClient(override);
18
- return new HttpPackRegistryClient(apiClient);
19
+ return new NpmPackRegistryClient();
19
20
  }
20
21
  function exitCodeForTestError(err) {
21
22
  if (err instanceof UnknownScopeError || err instanceof PackIntegrityError)
@@ -46,7 +47,7 @@ export function registerTestCommand(program) {
46
47
  const cwd = process.cwd();
47
48
  const config = resolveConfigFromBinding(cwd);
48
49
  const apiClient = new ApiClient(config);
49
- const registryClient = buildRegistryClient(apiClient);
50
+ const registryClient = buildRegistryClient();
50
51
  const cliVersion = getCliVersion();
51
52
  if (opts.list) {
52
53
  try {
@@ -7,16 +7,17 @@ import { resolveConfigFromBinding } from "@zivis/mcp/project-binding";
7
7
  import { requireApplicationId } from "@zivis/mcp/resolve-application-id";
8
8
  import { renderSecurityContext } from "../../internal/security-context.js";
9
9
  import { startThreatModelRun, syncThreatModel, } from "../../internal/threatmodel-run.js";
10
- import { HttpPackRegistryClient, LocalPackRegistryClient, registryDirOverride } from "../../internal/packs/registry-client.js";
10
+ import { LocalPackRegistryClient, registryDirOverride } from "../../internal/packs/registry-client.js";
11
+ import { NpmPackRegistryClient } from "../../internal/packs/npm-registry-client.js";
11
12
  import { PackUnavailableError, PackIntegrityError } from "../../internal/packs/types.js";
12
13
  import { jsonEnvelope, printJson, EXIT, exitCodeForError, errorMessage, failCommand } from "../../internal/cli-output.js";
13
14
  class UsageError extends Error {
14
15
  }
15
- function buildRegistryClient(apiClient) {
16
+ function buildRegistryClient() {
16
17
  const override = registryDirOverride();
17
18
  if (override)
18
19
  return new LocalPackRegistryClient(override);
19
- return new HttpPackRegistryClient(apiClient);
20
+ return new NpmPackRegistryClient();
20
21
  }
21
22
  function exitCodeForThreatModelError(err) {
22
23
  if (err instanceof PackIntegrityError)
@@ -56,7 +57,7 @@ export function registerThreatmodelCommand(program) {
56
57
  const cwd = process.cwd();
57
58
  const config = resolveConfigFromBinding(cwd);
58
59
  const apiClient = new ApiClient(config);
59
- const registryClient = buildRegistryClient(apiClient);
60
+ const registryClient = buildRegistryClient();
60
61
  const cliVersion = getCliVersion();
61
62
  try {
62
63
  const resolvedApp = requireApplicationId(opts.app, cwd);
@@ -105,7 +106,7 @@ export function registerThreatmodelCommand(program) {
105
106
  }
106
107
  const config = resolveConfigFromBinding(cwd);
107
108
  const apiClient = new ApiClient(config);
108
- const registryClient = buildRegistryClient(apiClient);
109
+ const registryClient = buildRegistryClient();
109
110
  try {
110
111
  const resolvedApp = requireApplicationId(opts.app, cwd);
111
112
  if (!resolvedApp.ok) {
package/dist/index.js CHANGED
@@ -7,7 +7,6 @@ import { registerAuthCommands } from "./commands/auth/index.js";
7
7
  import { initCommand as authInitCommand } from "./commands/auth/init.js";
8
8
  import { registerAppCommands } from "./commands/app/index.js";
9
9
  import { registerTargetCommands } from "./commands/target/index.js";
10
- import { registerTmCommands } from "./commands/tm/index.js";
11
10
  import { registerScanCommands } from "./commands/scan/index.js";
12
11
  import { registerCheckCommands } from "./commands/check/index.js";
13
12
  import { registerInspectCommands } from "./commands/inspect/index.js";
@@ -48,7 +47,6 @@ registerAuthCommands(program);
48
47
  registerCheckCommands(program);
49
48
  registerAppCommands(program);
50
49
  registerTargetCommands(program);
51
- registerTmCommands(program);
52
50
  registerScanCommands(program);
53
51
  registerInspectCommands(program);
54
52
  registerMcpCommands(program);
@@ -2,9 +2,9 @@ export type { PackType, PackContentEntry, PackScopeEntry, PackSignature, PackMan
2
2
  export { PackIntegrityError, PackUnavailableError, UnknownScopeError } from "./types.js";
3
3
  export { resolveCompatiblePack, resolveScope, listAvailableScopes } from "./resolve.js";
4
4
  export type { ResolveOptions } from "./resolve.js";
5
- export { HttpPackRegistryClient, LocalPackRegistryClient, registryDirOverride, } from "./registry-client.js";
6
- export type { MinimalApiClient } from "./registry-client.js";
5
+ export { LocalPackRegistryClient, registryDirOverride, } from "./registry-client.js";
6
+ export { NpmPackRegistryClient } from "./npm-registry-client.js";
7
7
  export { userCacheRoot, packsRoot, packTypeDir, packVersionDir } from "./local-paths.js";
8
- export { verifyManifestSignature, manifestSigningPayload, DEV_PACK_SIGNING_KEY_ID } from "./signing.js";
8
+ export { verifyManifestSignature, manifestSigningPayload, PRODUCTION_PACK_SIGNING_KEY_ID } from "./signing.js";
9
9
  export { sha256Hex, verifyContentHash, verifyPack } from "./integrity.js";
10
10
  export { compareVersions, satisfiesMinVersion, parseVersion } from "./semver.js";
@@ -1,7 +1,8 @@
1
1
  export { PackIntegrityError, PackUnavailableError, UnknownScopeError } from "./types.js";
2
2
  export { resolveCompatiblePack, resolveScope, listAvailableScopes } from "./resolve.js";
3
- export { HttpPackRegistryClient, LocalPackRegistryClient, registryDirOverride, } from "./registry-client.js";
3
+ export { LocalPackRegistryClient, registryDirOverride, } from "./registry-client.js";
4
+ export { NpmPackRegistryClient } from "./npm-registry-client.js";
4
5
  export { userCacheRoot, packsRoot, packTypeDir, packVersionDir } from "./local-paths.js";
5
- export { verifyManifestSignature, manifestSigningPayload, DEV_PACK_SIGNING_KEY_ID } from "./signing.js";
6
+ export { verifyManifestSignature, manifestSigningPayload, PRODUCTION_PACK_SIGNING_KEY_ID } from "./signing.js";
6
7
  export { sha256Hex, verifyContentHash, verifyPack } from "./integrity.js";
7
8
  export { compareVersions, satisfiesMinVersion, parseVersion } from "./semver.js";
@@ -0,0 +1,14 @@
1
+ import type { PackManifestV1, PackRegistryClient, PackType, PackVersionListing } from "./types.js";
2
+ export declare class NpmPackRegistryClient implements PackRegistryClient {
3
+ private readonly registryUrl;
4
+ private readonly packumentCache;
5
+ private readonly extractionCache;
6
+ constructor(registryUrl?: string);
7
+ private packageName;
8
+ private fetchPackument;
9
+ listVersions(packType: PackType): Promise<PackVersionListing>;
10
+ private ensureExtracted;
11
+ private downloadAndExtract;
12
+ getManifest(packType: PackType, version: string): Promise<PackManifestV1>;
13
+ getContent(packType: PackType, version: string, relativePath: string): Promise<string>;
14
+ }
@@ -0,0 +1,87 @@
1
+ import * as fsp from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import * as zlib from "node:zlib";
5
+ import { pipeline } from "node:stream/promises";
6
+ import { Readable } from "node:stream";
7
+ import * as tar from "tar";
8
+ const NPM_PACKAGE_NAMES = {
9
+ appsec: "@zivis/appsec-pack",
10
+ threatmodel: "@zivis/threatmodel-pack",
11
+ };
12
+ function encodeScopedName(name) {
13
+ return name.replaceAll("/", "%2f");
14
+ }
15
+ export class NpmPackRegistryClient {
16
+ registryUrl;
17
+ packumentCache = new Map();
18
+ extractionCache = new Map();
19
+ constructor(registryUrl = "https://registry.npmjs.org") {
20
+ this.registryUrl = registryUrl;
21
+ }
22
+ packageName(packType) {
23
+ const name = NPM_PACKAGE_NAMES[packType];
24
+ if (!name)
25
+ throw new Error(`No npm package mapping for pack type "${packType}"`);
26
+ return name;
27
+ }
28
+ async fetchPackument(packType) {
29
+ let cached = this.packumentCache.get(packType);
30
+ if (!cached) {
31
+ cached = (async () => {
32
+ const url = `${this.registryUrl}/${encodeScopedName(this.packageName(packType))}`;
33
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
34
+ if (!res.ok) {
35
+ throw new Error(`npm registry returned HTTP ${res.status} for ${url}`);
36
+ }
37
+ return (await res.json());
38
+ })();
39
+ this.packumentCache.set(packType, cached);
40
+ }
41
+ return cached;
42
+ }
43
+ async listVersions(packType) {
44
+ const packument = await this.fetchPackument(packType);
45
+ const versions = Object.values(packument.versions)
46
+ .filter((v) => v.zivisPack?.minCliVersion && v.zivisPack.packType === packType)
47
+ .map((v) => ({
48
+ version: v.version,
49
+ min_cli_version: v.zivisPack.minCliVersion,
50
+ manifest_sha256: v.dist.shasum ?? "",
51
+ published_at: packument.time?.[v.version] ?? "",
52
+ }));
53
+ return { pack_type: packType, versions };
54
+ }
55
+ async ensureExtracted(packType, version) {
56
+ const key = `${packType}@${version}`;
57
+ let cached = this.extractionCache.get(key);
58
+ if (!cached) {
59
+ cached = this.downloadAndExtract(packType, version);
60
+ this.extractionCache.set(key, cached);
61
+ }
62
+ return cached;
63
+ }
64
+ async downloadAndExtract(packType, version) {
65
+ const packument = await this.fetchPackument(packType);
66
+ const versionMeta = packument.versions[version];
67
+ if (!versionMeta) {
68
+ throw new Error(`${this.packageName(packType)}@${version} not found on the npm registry`);
69
+ }
70
+ const tarballRes = await fetch(versionMeta.dist.tarball);
71
+ if (!tarballRes.ok || !tarballRes.body) {
72
+ throw new Error(`Failed to download tarball for ${this.packageName(packType)}@${version}: HTTP ${tarballRes.status}`);
73
+ }
74
+ const extractDir = await fsp.mkdtemp(path.join(os.tmpdir(), "zivis-pack-"));
75
+ await pipeline(Readable.fromWeb(tarballRes.body), zlib.createGunzip(), tar.extract({ cwd: extractDir, strip: 1 }));
76
+ return extractDir;
77
+ }
78
+ async getManifest(packType, version) {
79
+ const extractDir = await this.ensureExtracted(packType, version);
80
+ const raw = await fsp.readFile(path.join(extractDir, "dist", "manifest.json"), "utf-8");
81
+ return JSON.parse(raw);
82
+ }
83
+ async getContent(packType, version, relativePath) {
84
+ const extractDir = await this.ensureExtracted(packType, version);
85
+ return fsp.readFile(path.join(extractDir, "dist", relativePath), "utf-8");
86
+ }
87
+ }
@@ -1,15 +1,4 @@
1
1
  import type { PackManifestV1, PackRegistryClient, PackType, PackVersionListing } from "./types.js";
2
- export interface MinimalApiClient {
3
- get<T>(path: string): Promise<T>;
4
- getText(path: string): Promise<string>;
5
- }
6
- export declare class HttpPackRegistryClient implements PackRegistryClient {
7
- private readonly apiClient;
8
- constructor(apiClient: MinimalApiClient);
9
- listVersions(packType: PackType): Promise<PackVersionListing>;
10
- getManifest(packType: PackType, version: string): Promise<PackManifestV1>;
11
- getContent(packType: PackType, version: string, relativePath: string): Promise<string>;
12
- }
13
2
  export declare class LocalPackRegistryClient implements PackRegistryClient {
14
3
  private readonly rootDir;
15
4
  constructor(rootDir: string);
@@ -1,20 +1,5 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- export class HttpPackRegistryClient {
4
- apiClient;
5
- constructor(apiClient) {
6
- this.apiClient = apiClient;
7
- }
8
- listVersions(packType) {
9
- return this.apiClient.get(`/api/zivis-packs/${packType}`);
10
- }
11
- getManifest(packType, version) {
12
- return this.apiClient.get(`/api/zivis-packs/${packType}/${version}/manifest.json`);
13
- }
14
- getContent(packType, version, relativePath) {
15
- return this.apiClient.getText(`/api/zivis-packs/${packType}/${version}/content/${relativePath}`);
16
- }
17
- }
18
3
  export class LocalPackRegistryClient {
19
4
  rootDir;
20
5
  constructor(rootDir) {
@@ -1,5 +1,5 @@
1
1
  import type { PackManifestV1, PackSignature } from "./types.js";
2
- export declare const DEV_PACK_SIGNING_KEY_ID = "zivis-pack-dev-2026-08";
2
+ export declare const PRODUCTION_PACK_SIGNING_KEY_ID = "zivis-pack-prod-2026-08";
3
3
  export declare function manifestSigningPayload(manifest: Omit<PackManifestV1, "signature">): string;
4
4
  export declare function verifyManifestSignature(manifest: PackManifestV1): boolean;
5
- export declare function signManifestDev(manifest: Omit<PackManifestV1, "signature">, privateKeySpkiB64: string): PackSignature;
5
+ export declare function signManifest(manifest: Omit<PackManifestV1, "signature">, privateKeySpkiB64: string, keyId: string): PackSignature;
@@ -1,12 +1,19 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import { canonicalize } from "./jcs.js";
3
- export const DEV_PACK_SIGNING_KEY_ID = "zivis-pack-dev-2026-08";
4
- const DEV_PACK_PUBLIC_KEY_SPKI_B64 = "MCowBQYDK2VwAyEA1xfF9X57KKlqMA8vbHg7DEuU6K5aTURJaFINYk6wRvA=";
3
+ export const PRODUCTION_PACK_SIGNING_KEY_ID = "zivis-pack-prod-2026-08";
4
+ const PRODUCTION_PACK_PUBLIC_KEY_SPKI_B64 = "MCowBQYDK2VwAyEAxp1BFtbclfI8mB7NV4hwQ2Gx5OcSM3Irj+w4oZrooss=";
5
5
  const KNOWN_PUBLIC_KEYS = {
6
- [DEV_PACK_SIGNING_KEY_ID]: DEV_PACK_PUBLIC_KEY_SPKI_B64,
6
+ [PRODUCTION_PACK_SIGNING_KEY_ID]: PRODUCTION_PACK_PUBLIC_KEY_SPKI_B64,
7
7
  };
8
+ function loadTestOverrideKey(keyId) {
9
+ const testKeyId = process.env.ZIVIS_PACK_VERIFY_TEST_KEY_ID;
10
+ const testPublicKey = process.env.ZIVIS_PACK_VERIFY_TEST_PUBLIC_KEY;
11
+ if (testKeyId && testPublicKey && testKeyId === keyId)
12
+ return testPublicKey;
13
+ return null;
14
+ }
8
15
  function loadPublicKey(keyId) {
9
- const spkiB64 = KNOWN_PUBLIC_KEYS[keyId];
16
+ const spkiB64 = KNOWN_PUBLIC_KEYS[keyId] ?? loadTestOverrideKey(keyId);
10
17
  if (!spkiB64)
11
18
  return null;
12
19
  return crypto.createPublicKey({
@@ -33,7 +40,7 @@ export function verifyManifestSignature(manifest) {
33
40
  return false;
34
41
  }
35
42
  }
36
- export function signManifestDev(manifest, privateKeySpkiB64) {
43
+ export function signManifest(manifest, privateKeySpkiB64, keyId) {
37
44
  const privateKey = crypto.createPrivateKey({
38
45
  key: Buffer.from(privateKeySpkiB64, "base64"),
39
46
  format: "der",
@@ -43,7 +50,7 @@ export function signManifestDev(manifest, privateKeySpkiB64) {
43
50
  const sig = crypto.sign(null, new Uint8Array(Buffer.from(payload, "utf8")), privateKey);
44
51
  return {
45
52
  algorithm: "ed25519",
46
- key_id: DEV_PACK_SIGNING_KEY_ID,
53
+ key_id: keyId,
47
54
  signature: sig.toString("base64"),
48
55
  };
49
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zivis/cli",
3
- "version": "0.1.0-alpha.44",
3
+ "version": "0.1.0-alpha.45",
4
4
  "description": "ZIVIS CLI — threat modeling, scans, and MCP server for IDE integration",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://zivis.ai",
@@ -32,8 +32,9 @@
32
32
  "ignore": "^7.0.5",
33
33
  "js-yaml": "^4.1.0",
34
34
  "open": "^10.1.0",
35
+ "tar": "^7.5.22",
35
36
  "web-tree-sitter": "^0.26.8",
36
- "@zivis/mcp": "0.1.6"
37
+ "@zivis/mcp": "0.1.7"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@types/js-yaml": "^4.0.9",
@@ -1,2 +0,0 @@
1
- import type { Command } from "commander";
2
- export declare function registerTmCommands(program: Command): void;
@@ -1,263 +0,0 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
- import { ApiClient } from "@zivis/mcp/api-client";
4
- import { resolveConfigFromBinding } from "@zivis/mcp/project-binding";
5
- import { getEphemeralCreds } from "../../internal/target-auth/index.js";
6
- import { runExtractor } from "../../internal/extractor/index.js";
7
- import { ensureZivisGitignore } from "../../internal/zivis-local-state.js";
8
- function buildClient(opts) {
9
- const config = opts.session
10
- ? { ...resolveConfigFromBinding(process.cwd()), session: opts.session }
11
- : resolveConfigFromBinding(process.cwd());
12
- return new ApiClient(config);
13
- }
14
- function collect(value, previous) {
15
- return previous.concat([value]);
16
- }
17
- function defaultOutPath(extractedAt) {
18
- const ts = extractedAt.replace(/[:.]/g, "-");
19
- return `.zivis/extracted/${ts}.json`;
20
- }
21
- export function registerTmCommands(program) {
22
- const tm = program.command("tm").description("threat model operations");
23
- tm.command("list")
24
- .description("list threat models in the active org")
25
- .option("-s, --session <name>", "named session")
26
- .option("--app <applicationId>", "filter by application id")
27
- .option("--json", "emit raw JSON")
28
- .action(async (opts) => {
29
- const client = buildClient(opts);
30
- const query = opts.app ? `?applicationId=${encodeURIComponent(opts.app)}` : "";
31
- const list = await client.request("GET", `/api/threat-models${query}`);
32
- if (opts.json) {
33
- process.stdout.write(JSON.stringify(list, null, 2) + "\n");
34
- return;
35
- }
36
- if (list.length === 0) {
37
- console.log("No threat models found.");
38
- return;
39
- }
40
- for (const tm of list) {
41
- const v = tm.latestVersion;
42
- const stats = v
43
- ? `v${v.versionNumber} ${v.analysisStatus} · ${v.totalThreats} threats (${v.criticalCount}C/${v.highCount}H)`
44
- : "no analysis";
45
- const app = tm.applicationName ? ` [${tm.applicationName}]` : "";
46
- console.log(`${tm.id} ${tm.name}${app}`);
47
- console.log(` ${tm.status} · ${stats}`);
48
- }
49
- });
50
- tm.command("get <threatModelId>")
51
- .description("dump a threat model as JSON")
52
- .option("-s, --session <name>", "named session")
53
- .action(async (id, opts) => {
54
- const client = buildClient(opts);
55
- const tm = await client.request("GET", `/api/threat-models/${encodeURIComponent(id)}`);
56
- process.stdout.write(JSON.stringify(tm, null, 2) + "\n");
57
- });
58
- tm.command("pull <threatModelId>")
59
- .description("write a threat model to a local JSON file (default: ./<id>.json)")
60
- .option("-s, --session <name>", "named session")
61
- .option("-o, --out <file>", "output file path")
62
- .action(async (id, opts) => {
63
- const client = buildClient(opts);
64
- const tm = await client.request("GET", `/api/threat-models/${encodeURIComponent(id)}`);
65
- const outPath = path.resolve(opts.out ?? `${id}.json`);
66
- await fs.writeFile(outPath, JSON.stringify(tm, null, 2) + "\n", "utf8");
67
- console.log(`✓ Wrote ${outPath}`);
68
- if (tm.name)
69
- console.log(` ${tm.name}`);
70
- });
71
- tm.command("push <threatModelId>")
72
- .description("update a threat model from a local JSON file (only name/description/status fields)")
73
- .option("-s, --session <name>", "named session")
74
- .option("-f, --file <file>", "JSON file to read (default: ./<id>.json)")
75
- .option("--dry-run", "print the PATCH payload without sending")
76
- .action(async (id, opts) => {
77
- const inPath = path.resolve(opts.file ?? `${id}.json`);
78
- const raw = await fs.readFile(inPath, "utf8").catch((err) => {
79
- console.error(`Could not read ${inPath}: ${err.message}`);
80
- process.exit(1);
81
- });
82
- const parsed = JSON.parse(raw);
83
- const payload = {};
84
- for (const k of ["name", "description", "status", "campaignId"]) {
85
- if (parsed[k] !== undefined)
86
- payload[k] = parsed[k];
87
- }
88
- if (Object.keys(payload).length === 0) {
89
- console.error("No updatable fields found in file (name/description/status/campaignId).");
90
- process.exit(1);
91
- }
92
- if (opts.dryRun) {
93
- console.log("Would PATCH:");
94
- console.log(JSON.stringify(payload, null, 2));
95
- return;
96
- }
97
- const client = buildClient(opts);
98
- const updated = await client.request("PATCH", `/api/threat-models/${encodeURIComponent(id)}`, payload);
99
- console.log(`✓ Updated ${updated.id}`);
100
- if (updated.name)
101
- console.log(` ${updated.name}`);
102
- });
103
- tm.command("extract")
104
- .description("walk the local repo with tree-sitter and emit structural metadata as JSON (Phase J.2)")
105
- .option("--root <dir>", "directory to scan (default: cwd)")
106
- .option("--out <file>", "write JSON to a file (default: .zivis/extracted/<ts>.json; '-' for stdout)")
107
- .option("--ignore <pattern>", "extra ignore pattern (repeatable)", collect, [])
108
- .option("--privacy-audit", "print exactly what would be sent without sending — never makes network calls")
109
- .option("--lang <list>", "comma-separated subset of adapters to run: typescript,go,python")
110
- .action(async (opts) => {
111
- const rootDir = path.resolve(opts.root ?? process.cwd());
112
- const cliPkg = JSON.parse(await fs.readFile(path.join(path.dirname(new URL(import.meta.url).pathname), "../../../package.json"), "utf8"));
113
- const langs = opts.lang
114
- ? String(opts.lang).split(",").map((s) => s.trim()).filter(Boolean)
115
- : undefined;
116
- const result = await runExtractor({
117
- rootDir,
118
- languages: langs,
119
- extraIgnores: opts.ignore,
120
- extractorVersion: cliPkg.version,
121
- });
122
- if (opts.privacyAudit) {
123
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
124
- process.stderr.write(`\n[zivis] Privacy audit: ${result.scope.filesScanned} files scanned, ` +
125
- `${result.components.length} component(s), ${result.externalServices.length} external service(s), ` +
126
- `${result.dataStores.length} data store(s), ${result.manifests.length} manifest(s).\n` +
127
- `[zivis] Nothing was sent. Run without --privacy-audit + --apply <tm-id> to upload.\n`);
128
- return;
129
- }
130
- const target = opts.out ?? defaultOutPath(result.extractedAt);
131
- if (target === "-") {
132
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
133
- return;
134
- }
135
- const absOut = path.isAbsolute(target) ? target : path.join(rootDir, target);
136
- if (absOut.startsWith(path.join(rootDir, ".zivis") + path.sep)) {
137
- ensureZivisGitignore(rootDir);
138
- }
139
- await fs.mkdir(path.dirname(absOut), { recursive: true });
140
- await fs.writeFile(absOut, JSON.stringify(result, null, 2) + "\n", "utf8");
141
- console.log(`✓ Wrote ${absOut}`);
142
- console.log(` ${result.scope.filesScanned} files scanned in ${result.scope.durationMs}ms — ` +
143
- `${result.components.length} component(s), ${result.externalServices.length} external service(s), ` +
144
- `${result.dataStores.length} data store(s).`);
145
- });
146
- tm.command("threats")
147
- .description("show threats touching a file (Phase J.3 — IDE/CLI surface)")
148
- .requiredOption("--tm <threatModelId>", "threat model to query")
149
- .requiredOption("--file <path>", "file path (repo-relative, matches ThreatModelComponent.sourceFiles)")
150
- .option("-s, --session <name>", "named session")
151
- .option("--json", "emit raw JSON")
152
- .action(async (opts) => {
153
- const client = buildClient(opts);
154
- const path = `/api/threat-models/${encodeURIComponent(opts.tm)}/files/threats?path=${encodeURIComponent(opts.file)}`;
155
- const result = await client.request("GET", path);
156
- if (opts.json) {
157
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
158
- return;
159
- }
160
- if (result.components.length === 0) {
161
- console.log(`No components reference ${result.path} in this threat model.`);
162
- return;
163
- }
164
- console.log(`${result.path}`);
165
- for (const c of result.components) {
166
- const idLabel = c.displayId ? `[${c.displayId}]` : `[${c.id.slice(0, 8)}]`;
167
- console.log(` ${idLabel} ${c.name} (${c.componentType}) — ${c.threatCount} threat(s)`);
168
- for (const t of c.threats) {
169
- const sev = t.severity ? `[${t.severity}]` : "";
170
- console.log(` • ${sev} ${t.title ?? t.id}`);
171
- }
172
- }
173
- });
174
- const scenario = tm
175
- .command("scenario")
176
- .description("threat model scenario operations");
177
- scenario
178
- .command("list")
179
- .description("list scenarios in a threat model")
180
- .requiredOption("--tm <threatModelId>", "threat model id")
181
- .option("-s, --session <name>", "named session")
182
- .option("--json", "emit raw JSON")
183
- .action(async (opts) => {
184
- const client = buildClient(opts);
185
- const result = await client.request("GET", `/api/threat-models/${encodeURIComponent(opts.tm)}/scenarios`);
186
- const scenarios = Array.isArray(result) ? result : result.data ?? [];
187
- if (opts.json) {
188
- process.stdout.write(JSON.stringify(scenarios, null, 2) + "\n");
189
- return;
190
- }
191
- if (scenarios.length === 0) {
192
- console.log("No scenarios in this threat model.");
193
- return;
194
- }
195
- for (const s of scenarios) {
196
- const sev = s.severity ? ` [${s.severity}]` : "";
197
- console.log(`${s.id} ${s.status.padEnd(10)} ${s.title ?? "(untitled)"}${sev}`);
198
- }
199
- });
200
- scenario
201
- .command("show <scenarioId>")
202
- .description("dump a scenario as JSON (uses /api/scenarios/:id, no --tm needed)")
203
- .option("-s, --session <name>", "named session")
204
- .action(async (scenarioId, opts) => {
205
- const client = buildClient(opts);
206
- const result = await client.request("GET", `/api/scenarios/${encodeURIComponent(scenarioId)}`);
207
- process.stdout.write(JSON.stringify(result.data ?? result, null, 2) + "\n");
208
- });
209
- scenario
210
- .command("run <scenarioId>")
211
- .description("trigger a manual scenario run (scenario must be in 'testing' status)")
212
- .option("-s, --session <name>", "named session")
213
- .option("--app <applicationId>", "the customer-facing Application to test against (Phase J.4 — preferred over --target)")
214
- .option("--target <targetId>", "[deprecated alias for --app] interpreted as applicationId; also used as keychain key for ephemeral creds")
215
- .option("-p, --persona <name>", "target-system persona to use (default: default)", "default")
216
- .option("--no-creds", "skip the ephemeral-creds path even if --app is set")
217
- .option("--json", "emit raw JSON")
218
- .action(async (scenarioId, opts) => {
219
- const client = buildClient(opts);
220
- const path = `/api/scenarios/${encodeURIComponent(scenarioId)}/run`;
221
- const appId = opts.app ?? opts.target;
222
- if (opts.target && !opts.app) {
223
- console.warn("[zivis] --target is deprecated; use --app <applicationId>");
224
- }
225
- const body = {};
226
- if (appId) {
227
- body.applicationId = appId;
228
- if (opts.creds !== false) {
229
- const creds = await getEphemeralCreds(appId, opts.persona);
230
- if (!creds) {
231
- console.error(`No valid token for application ${appId} (persona: ${opts.persona}).`);
232
- console.error(`Run: zivis app auth ${appId} -p ${opts.persona}`);
233
- process.exit(2);
234
- }
235
- body.targetCreds = creds;
236
- }
237
- }
238
- try {
239
- const result = await client.request("POST", path, Object.keys(body).length ? body : undefined);
240
- if (opts.json) {
241
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
242
- return;
243
- }
244
- console.log(`✓ Triggered scenario run ${result.data.runId}`);
245
- if (result.threatModelId) {
246
- console.log(` Threat model: ${result.threatModelId}`);
247
- }
248
- if (result.credsAccepted) {
249
- console.log(` Ephemeral creds accepted (persona: ${opts.persona}, ttl: ${result.credsTtlSec ?? "?"}s)`);
250
- }
251
- if (result.scanId) {
252
- console.log(` Dispatched RT scan: ${result.scanId}`);
253
- }
254
- else if (result.dispatchSkippedReason) {
255
- console.log(` Dispatch: ${result.dispatchSkippedReason}`);
256
- }
257
- }
258
- catch (err) {
259
- console.error("Run failed:", err instanceof Error ? err.message : err);
260
- process.exit(1);
261
- }
262
- });
263
- }