@zivis/cli 0.1.0-alpha.40 → 0.1.0-alpha.41
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/dist/commands/app/index.js +4 -2
- package/dist/commands/assurance/index.d.ts +33 -0
- package/dist/commands/assurance/index.js +149 -0
- package/dist/commands/auth/index.js +1 -0
- package/dist/commands/auth/init.d.ts +1 -0
- package/dist/commands/auth/init.js +12 -8
- package/dist/commands/gate/index.d.ts +2 -0
- package/dist/commands/gate/index.js +171 -0
- package/dist/commands/mcp/index.js +33 -1
- package/dist/commands/run/index.d.ts +2 -0
- package/dist/commands/run/index.js +111 -0
- package/dist/commands/sync/index.d.ts +2 -0
- package/dist/commands/sync/index.js +187 -0
- package/dist/commands/test/index.d.ts +2 -0
- package/dist/commands/test/index.js +115 -0
- package/dist/commands/tm/index.js +4 -0
- package/dist/index.js +11 -0
- package/dist/internal/application-binding.d.ts +8 -0
- package/dist/internal/application-binding.js +167 -0
- package/dist/internal/cli-output.d.ts +16 -0
- package/dist/internal/cli-output.js +39 -0
- package/dist/internal/devx-run.d.ts +47 -0
- package/dist/internal/devx-run.js +36 -0
- package/dist/internal/gate-evaluate.d.ts +50 -0
- package/dist/internal/gate-evaluate.js +111 -0
- package/dist/internal/gate-policy.d.ts +38 -0
- package/dist/internal/gate-policy.js +167 -0
- package/dist/internal/git-metadata.d.ts +8 -0
- package/dist/internal/git-metadata.js +38 -0
- package/dist/internal/git.d.ts +8 -0
- package/dist/internal/git.js +30 -0
- package/dist/internal/ide-setup.d.ts +1 -0
- package/dist/internal/ide-setup.js +76 -19
- package/dist/internal/inventory-sync.d.ts +74 -0
- package/dist/internal/inventory-sync.js +189 -0
- package/dist/internal/packs/cache.d.ts +8 -0
- package/dist/internal/packs/cache.js +60 -0
- package/dist/internal/packs/index.d.ts +10 -0
- package/dist/internal/packs/index.js +7 -0
- package/dist/internal/packs/integrity.d.ts +9 -0
- package/dist/internal/packs/integrity.js +24 -0
- package/dist/internal/packs/jcs.d.ts +2 -0
- package/dist/internal/packs/jcs.js +57 -0
- package/dist/internal/packs/local-paths.d.ts +4 -0
- package/dist/internal/packs/local-paths.js +26 -0
- package/dist/internal/packs/registry-client.d.ts +20 -0
- package/dist/internal/packs/registry-client.js +40 -0
- package/dist/internal/packs/resolve.d.ts +8 -0
- package/dist/internal/packs/resolve.js +89 -0
- package/dist/internal/packs/semver.d.ts +9 -0
- package/dist/internal/packs/semver.js +57 -0
- package/dist/internal/packs/signing.d.ts +5 -0
- package/dist/internal/packs/signing.js +49 -0
- package/dist/internal/packs/types.d.ts +70 -0
- package/dist/internal/packs/types.js +20 -0
- package/dist/internal/run-workdir.d.ts +1 -0
- package/dist/internal/run-workdir.js +11 -0
- package/dist/internal/security-context.d.ts +62 -0
- package/dist/internal/security-context.js +50 -0
- package/dist/internal/sync-outbox.d.ts +40 -0
- package/dist/internal/sync-outbox.js +66 -0
- package/dist/internal/test-scope.d.ts +68 -0
- package/dist/internal/test-scope.js +69 -0
- package/dist/internal/zivis-local-state.d.ts +1 -0
- package/dist/internal/zivis-local-state.js +22 -0
- package/package.json +3 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
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=";
|
|
5
|
+
const KNOWN_PUBLIC_KEYS = {
|
|
6
|
+
[DEV_PACK_SIGNING_KEY_ID]: DEV_PACK_PUBLIC_KEY_SPKI_B64,
|
|
7
|
+
};
|
|
8
|
+
function loadPublicKey(keyId) {
|
|
9
|
+
const spkiB64 = KNOWN_PUBLIC_KEYS[keyId];
|
|
10
|
+
if (!spkiB64)
|
|
11
|
+
return null;
|
|
12
|
+
return crypto.createPublicKey({
|
|
13
|
+
key: Buffer.from(spkiB64, "base64"),
|
|
14
|
+
format: "der",
|
|
15
|
+
type: "spki",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function manifestSigningPayload(manifest) {
|
|
19
|
+
return canonicalize(manifest);
|
|
20
|
+
}
|
|
21
|
+
export function verifyManifestSignature(manifest) {
|
|
22
|
+
const { signature, ...unsigned } = manifest;
|
|
23
|
+
if (!signature || signature.algorithm !== "ed25519")
|
|
24
|
+
return false;
|
|
25
|
+
const publicKey = loadPublicKey(signature.key_id);
|
|
26
|
+
if (!publicKey)
|
|
27
|
+
return false;
|
|
28
|
+
try {
|
|
29
|
+
const payload = manifestSigningPayload(unsigned);
|
|
30
|
+
return crypto.verify(null, new Uint8Array(Buffer.from(payload, "utf8")), publicKey, new Uint8Array(Buffer.from(signature.signature, "base64")));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function signManifestDev(manifest, privateKeySpkiB64) {
|
|
37
|
+
const privateKey = crypto.createPrivateKey({
|
|
38
|
+
key: Buffer.from(privateKeySpkiB64, "base64"),
|
|
39
|
+
format: "der",
|
|
40
|
+
type: "pkcs8",
|
|
41
|
+
});
|
|
42
|
+
const payload = manifestSigningPayload(manifest);
|
|
43
|
+
const sig = crypto.sign(null, new Uint8Array(Buffer.from(payload, "utf8")), privateKey);
|
|
44
|
+
return {
|
|
45
|
+
algorithm: "ed25519",
|
|
46
|
+
key_id: DEV_PACK_SIGNING_KEY_ID,
|
|
47
|
+
signature: sig.toString("base64"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type PackType = "appsec" | "threatmodel";
|
|
2
|
+
export interface PackContentEntry {
|
|
3
|
+
path: string;
|
|
4
|
+
sha256: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PackScopeEntry extends PackContentEntry {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PackSignature {
|
|
12
|
+
algorithm: "ed25519";
|
|
13
|
+
key_id: string;
|
|
14
|
+
signature: string;
|
|
15
|
+
}
|
|
16
|
+
export interface PackManifestV1 {
|
|
17
|
+
schema_version: "1.0.0";
|
|
18
|
+
pack_id: string;
|
|
19
|
+
pack_type: PackType;
|
|
20
|
+
version: string;
|
|
21
|
+
min_cli_version: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
built_at: string;
|
|
24
|
+
base: PackContentEntry;
|
|
25
|
+
scopes?: PackScopeEntry[];
|
|
26
|
+
signature: PackSignature;
|
|
27
|
+
}
|
|
28
|
+
export interface PackVersionSummary {
|
|
29
|
+
version: string;
|
|
30
|
+
min_cli_version: string;
|
|
31
|
+
manifest_sha256: string;
|
|
32
|
+
published_at: string;
|
|
33
|
+
}
|
|
34
|
+
export interface PackVersionListing {
|
|
35
|
+
pack_type: PackType;
|
|
36
|
+
versions: PackVersionSummary[];
|
|
37
|
+
}
|
|
38
|
+
export type PackSource = "network" | "cache-verified" | "cache-offline";
|
|
39
|
+
export interface ResolvedScope {
|
|
40
|
+
id: string;
|
|
41
|
+
label: string;
|
|
42
|
+
description: string;
|
|
43
|
+
content: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ResolvedPack {
|
|
46
|
+
packId: string;
|
|
47
|
+
packType: PackType;
|
|
48
|
+
version: string;
|
|
49
|
+
minCliVersion: string;
|
|
50
|
+
source: PackSource;
|
|
51
|
+
basePath: string;
|
|
52
|
+
baseContent: string;
|
|
53
|
+
scopes: PackScopeEntry[];
|
|
54
|
+
cacheDir: string;
|
|
55
|
+
}
|
|
56
|
+
export declare class PackIntegrityError extends Error {
|
|
57
|
+
constructor(message: string);
|
|
58
|
+
}
|
|
59
|
+
export declare class PackUnavailableError extends Error {
|
|
60
|
+
constructor(message: string);
|
|
61
|
+
}
|
|
62
|
+
export declare class UnknownScopeError extends Error {
|
|
63
|
+
readonly availableScopes: PackScopeEntry[];
|
|
64
|
+
constructor(scopeId: string, availableScopes: PackScopeEntry[]);
|
|
65
|
+
}
|
|
66
|
+
export interface PackRegistryClient {
|
|
67
|
+
listVersions(packType: PackType): Promise<PackVersionListing>;
|
|
68
|
+
getManifest(packType: PackType, version: string): Promise<PackManifestV1>;
|
|
69
|
+
getContent(packType: PackType, version: string, relativePath: string): Promise<string>;
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class PackIntegrityError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "PackIntegrityError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class PackUnavailableError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "PackUnavailableError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class UnknownScopeError extends Error {
|
|
14
|
+
availableScopes;
|
|
15
|
+
constructor(scopeId, availableScopes) {
|
|
16
|
+
super(`Unknown scope "${scopeId}". Available scopes: ${availableScopes.map((s) => s.id).join(", ") || "(none)"}`);
|
|
17
|
+
this.name = "UnknownScopeError";
|
|
18
|
+
this.availableScopes = availableScopes;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeRunWorkFile(cwd: string, runId: string, filename: string, data: unknown): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { ensureZivisGitignore } from "./zivis-local-state.js";
|
|
4
|
+
export function writeRunWorkFile(cwd, runId, filename, data) {
|
|
5
|
+
ensureZivisGitignore(cwd);
|
|
6
|
+
const dir = path.join(cwd, ".zivis", "work", runId);
|
|
7
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
8
|
+
const filePath = path.join(dir, filename);
|
|
9
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
10
|
+
return filePath;
|
|
11
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface SecurityContextFinding {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
severity: string;
|
|
5
|
+
status: string;
|
|
6
|
+
owaspId: string | null;
|
|
7
|
+
sourceSubtype: string;
|
|
8
|
+
resourceId: string | null;
|
|
9
|
+
firstDetectedAt: string;
|
|
10
|
+
dispositionAt?: string | null;
|
|
11
|
+
dispositionRef?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface SecurityContext {
|
|
14
|
+
application: {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
};
|
|
18
|
+
repo: {
|
|
19
|
+
repoFullName: string;
|
|
20
|
+
defaultBranch: string | null;
|
|
21
|
+
latestKnownCommitSha: string | null;
|
|
22
|
+
revisionSyncedAt: string | null;
|
|
23
|
+
} | null;
|
|
24
|
+
lastTestedGitSha: string | null;
|
|
25
|
+
lastThreatModeledGitSha: string | null;
|
|
26
|
+
openFindings: {
|
|
27
|
+
total: number;
|
|
28
|
+
truncated: boolean;
|
|
29
|
+
items: SecurityContextFinding[];
|
|
30
|
+
};
|
|
31
|
+
findingsAwaitingRetest: {
|
|
32
|
+
total: number;
|
|
33
|
+
truncated: boolean;
|
|
34
|
+
items: SecurityContextFinding[];
|
|
35
|
+
};
|
|
36
|
+
inconclusiveAreas: unknown[];
|
|
37
|
+
weakOrUncoveredAreas: unknown[];
|
|
38
|
+
latestThreatModel: {
|
|
39
|
+
version: number;
|
|
40
|
+
gitCommitSha: string | null;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
methodology: string | null;
|
|
43
|
+
} | null;
|
|
44
|
+
trustRoomUrl: string | null;
|
|
45
|
+
generatedAt: string;
|
|
46
|
+
gaps: {
|
|
47
|
+
field: string;
|
|
48
|
+
reason: string;
|
|
49
|
+
}[];
|
|
50
|
+
}
|
|
51
|
+
export interface SecurityContextApi {
|
|
52
|
+
get<T>(path: string): Promise<T>;
|
|
53
|
+
}
|
|
54
|
+
export type SecurityContextOutcome = {
|
|
55
|
+
available: true;
|
|
56
|
+
context: SecurityContext;
|
|
57
|
+
} | {
|
|
58
|
+
available: false;
|
|
59
|
+
reason: string;
|
|
60
|
+
};
|
|
61
|
+
export declare function fetchSecurityContext(client: SecurityContextApi, applicationId: string): Promise<SecurityContextOutcome>;
|
|
62
|
+
export declare function renderSecurityContext(outcome: SecurityContextOutcome, format?: "human" | "json"): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export async function fetchSecurityContext(client, applicationId) {
|
|
2
|
+
try {
|
|
3
|
+
const context = await client.get(`/api/rt/applications/${encodeURIComponent(applicationId)}/security-context`);
|
|
4
|
+
return { available: true, context };
|
|
5
|
+
}
|
|
6
|
+
catch (err) {
|
|
7
|
+
return { available: false, reason: err instanceof Error ? err.message : String(err) };
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function summarizeFinding(f) {
|
|
11
|
+
return ` - [${f.severity}] ${f.title} (${f.id})${f.owaspId ? ` — ${f.owaspId}` : ""}`;
|
|
12
|
+
}
|
|
13
|
+
export function renderSecurityContext(outcome, format = "human") {
|
|
14
|
+
if (format === "json") {
|
|
15
|
+
return JSON.stringify(outcome, null, 2);
|
|
16
|
+
}
|
|
17
|
+
if (!outcome.available) {
|
|
18
|
+
return `[zivis] No connected security context available (${outcome.reason}). Continuing with local-only context.`;
|
|
19
|
+
}
|
|
20
|
+
const c = outcome.context;
|
|
21
|
+
const lines = [];
|
|
22
|
+
lines.push(`ZIVIS connected security context — ${c.application.name}`);
|
|
23
|
+
if (c.repo) {
|
|
24
|
+
lines.push(`Last known repo commit: ${c.repo.latestKnownCommitSha ?? "unknown"} (${c.repo.repoFullName})`);
|
|
25
|
+
}
|
|
26
|
+
lines.push(c.lastThreatModeledGitSha
|
|
27
|
+
? `Last threat-modeled commit: ${c.lastThreatModeledGitSha}; compare local HEAD using git.`
|
|
28
|
+
: `No prior threat model on record for this Application.`);
|
|
29
|
+
if (c.lastTestedGitSha) {
|
|
30
|
+
lines.push(`Last tested commit: ${c.lastTestedGitSha}; compare local HEAD using git.`);
|
|
31
|
+
}
|
|
32
|
+
if (c.openFindings.total > 0) {
|
|
33
|
+
lines.push(`Prior open findings: ${c.openFindings.total}${c.openFindings.truncated ? ` (showing ${c.openFindings.items.length})` : ""}`);
|
|
34
|
+
c.openFindings.items.forEach((f) => lines.push(summarizeFinding(f)));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
lines.push(`No prior open findings on record.`);
|
|
38
|
+
}
|
|
39
|
+
if (c.findingsAwaitingRetest.total > 0) {
|
|
40
|
+
lines.push(`Findings claimed fixed but not yet retested: ${c.findingsAwaitingRetest.total}${c.findingsAwaitingRetest.truncated ? ` (showing ${c.findingsAwaitingRetest.items.length})` : ""}`);
|
|
41
|
+
c.findingsAwaitingRetest.items.forEach((f) => lines.push(` - Finding ${f.id} was remediated but has not been retested.${f.dispositionRef ? ` (ref: ${f.dispositionRef})` : ""}`));
|
|
42
|
+
}
|
|
43
|
+
if (c.latestThreatModel) {
|
|
44
|
+
lines.push(`Latest threat model: v${c.latestThreatModel.version}${c.latestThreatModel.methodology ? `, methodology: ${c.latestThreatModel.methodology}` : ""}`);
|
|
45
|
+
}
|
|
46
|
+
if (c.trustRoomUrl) {
|
|
47
|
+
lines.push(`Trust Room: ${c.trustRoomUrl}`);
|
|
48
|
+
}
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { InventorySyncInput, InventorySyncProvenance } from "./inventory-sync.js";
|
|
2
|
+
interface OutboxEntryBase {
|
|
3
|
+
id: string;
|
|
4
|
+
queuedAt: string;
|
|
5
|
+
attempts: number;
|
|
6
|
+
lastError: string;
|
|
7
|
+
}
|
|
8
|
+
export interface InventorySyncOutboxEntry extends OutboxEntryBase {
|
|
9
|
+
kind: "inventory-sync";
|
|
10
|
+
applicationId: string;
|
|
11
|
+
input: InventorySyncInput;
|
|
12
|
+
provenance: InventorySyncProvenance;
|
|
13
|
+
}
|
|
14
|
+
export interface DevXRunCompleteOutboxEntry extends OutboxEntryBase {
|
|
15
|
+
kind: "devx-run-complete";
|
|
16
|
+
runId: string;
|
|
17
|
+
contractVersion: string;
|
|
18
|
+
result: unknown;
|
|
19
|
+
}
|
|
20
|
+
export type OutboxEntry = InventorySyncOutboxEntry | DevXRunCompleteOutboxEntry;
|
|
21
|
+
export declare function writeOutboxEntry(cwd: string, params: {
|
|
22
|
+
applicationId: string;
|
|
23
|
+
input: InventorySyncInput;
|
|
24
|
+
provenance: InventorySyncProvenance;
|
|
25
|
+
error: string;
|
|
26
|
+
}): string;
|
|
27
|
+
export declare function writeDevXRunCompleteOutboxEntry(cwd: string, params: {
|
|
28
|
+
runId: string;
|
|
29
|
+
contractVersion: string;
|
|
30
|
+
result: unknown;
|
|
31
|
+
error: string;
|
|
32
|
+
}): string;
|
|
33
|
+
export interface OutboxListing {
|
|
34
|
+
filePath: string;
|
|
35
|
+
entry: OutboxEntry;
|
|
36
|
+
}
|
|
37
|
+
export declare function listOutboxEntries(cwd: string): OutboxListing[];
|
|
38
|
+
export declare function removeOutboxEntry(filePath: string): void;
|
|
39
|
+
export declare function recordOutboxRetryFailure(filePath: string, entry: OutboxEntry, error: string): void;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as crypto from "node:crypto";
|
|
4
|
+
import { ensureZivisGitignore } from "./zivis-local-state.js";
|
|
5
|
+
function outboxDir(cwd) {
|
|
6
|
+
return path.join(cwd, ".zivis", "outbox");
|
|
7
|
+
}
|
|
8
|
+
function writeEntryFile(cwd, entry) {
|
|
9
|
+
ensureZivisGitignore(cwd);
|
|
10
|
+
const dir = outboxDir(cwd);
|
|
11
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
12
|
+
const filePath = path.join(dir, `${entry.queuedAt.replace(/[:.]/g, "-")}-${entry.id.slice(0, 8)}.json`);
|
|
13
|
+
fs.writeFileSync(filePath, JSON.stringify(entry, null, 2) + "\n");
|
|
14
|
+
return filePath;
|
|
15
|
+
}
|
|
16
|
+
export function writeOutboxEntry(cwd, params) {
|
|
17
|
+
const entry = {
|
|
18
|
+
kind: "inventory-sync",
|
|
19
|
+
id: crypto.randomUUID(),
|
|
20
|
+
applicationId: params.applicationId,
|
|
21
|
+
input: params.input,
|
|
22
|
+
provenance: params.provenance,
|
|
23
|
+
queuedAt: new Date().toISOString(),
|
|
24
|
+
attempts: 1,
|
|
25
|
+
lastError: params.error,
|
|
26
|
+
};
|
|
27
|
+
return writeEntryFile(cwd, entry);
|
|
28
|
+
}
|
|
29
|
+
export function writeDevXRunCompleteOutboxEntry(cwd, params) {
|
|
30
|
+
const entry = {
|
|
31
|
+
kind: "devx-run-complete",
|
|
32
|
+
id: crypto.randomUUID(),
|
|
33
|
+
runId: params.runId,
|
|
34
|
+
contractVersion: params.contractVersion,
|
|
35
|
+
result: params.result,
|
|
36
|
+
queuedAt: new Date().toISOString(),
|
|
37
|
+
attempts: 1,
|
|
38
|
+
lastError: params.error,
|
|
39
|
+
};
|
|
40
|
+
return writeEntryFile(cwd, entry);
|
|
41
|
+
}
|
|
42
|
+
export function listOutboxEntries(cwd) {
|
|
43
|
+
const dir = outboxDir(cwd);
|
|
44
|
+
if (!fs.existsSync(dir))
|
|
45
|
+
return [];
|
|
46
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json")).sort();
|
|
47
|
+
const listings = [];
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
const filePath = path.join(dir, file);
|
|
50
|
+
try {
|
|
51
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
52
|
+
const entry = (raw.kind ? raw : { ...raw, kind: "inventory-sync" });
|
|
53
|
+
listings.push({ filePath, entry });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return listings;
|
|
59
|
+
}
|
|
60
|
+
export function removeOutboxEntry(filePath) {
|
|
61
|
+
fs.rmSync(filePath, { force: true });
|
|
62
|
+
}
|
|
63
|
+
export function recordOutboxRetryFailure(filePath, entry, error) {
|
|
64
|
+
const updated = { ...entry, attempts: entry.attempts + 1, lastError: error };
|
|
65
|
+
fs.writeFileSync(filePath, JSON.stringify(updated, null, 2) + "\n");
|
|
66
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type DevXRunApi } from "./devx-run.js";
|
|
2
|
+
import { type SecurityContextApi, type SecurityContextOutcome } from "./security-context.js";
|
|
3
|
+
import type { PackRegistryClient } from "./packs/types.js";
|
|
4
|
+
export interface TestRunDeps {
|
|
5
|
+
apiClient: DevXRunApi & SecurityContextApi;
|
|
6
|
+
registryClient: PackRegistryClient;
|
|
7
|
+
}
|
|
8
|
+
export interface StartTestRunParams {
|
|
9
|
+
applicationId: string;
|
|
10
|
+
scopeId?: string;
|
|
11
|
+
cwd: string;
|
|
12
|
+
cliVersion: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ResolvedScopeSummary {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
export interface TestRunEnvelope {
|
|
19
|
+
run_id: string;
|
|
20
|
+
application: {
|
|
21
|
+
id: string;
|
|
22
|
+
};
|
|
23
|
+
connection: {
|
|
24
|
+
mode: "connected";
|
|
25
|
+
};
|
|
26
|
+
git: {
|
|
27
|
+
sha: string | null;
|
|
28
|
+
dirty: boolean | null;
|
|
29
|
+
};
|
|
30
|
+
methodology: {
|
|
31
|
+
pack: string;
|
|
32
|
+
version: string;
|
|
33
|
+
path: string;
|
|
34
|
+
scope?: {
|
|
35
|
+
id: string;
|
|
36
|
+
label: string;
|
|
37
|
+
path: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
context: {
|
|
41
|
+
path: string;
|
|
42
|
+
};
|
|
43
|
+
scope: ResolvedScopeSummary | null;
|
|
44
|
+
requested_intent: {
|
|
45
|
+
scope: string;
|
|
46
|
+
} | null;
|
|
47
|
+
result_contract: {
|
|
48
|
+
schema_version: string;
|
|
49
|
+
complete_command: string;
|
|
50
|
+
};
|
|
51
|
+
agent_instructions: string[];
|
|
52
|
+
}
|
|
53
|
+
export interface StartTestRunResult {
|
|
54
|
+
envelope: TestRunEnvelope;
|
|
55
|
+
resolvedScope: ResolvedScopeSummary | null;
|
|
56
|
+
contextOutcome: SecurityContextOutcome;
|
|
57
|
+
}
|
|
58
|
+
export declare function startTestRun(deps: TestRunDeps, params: StartTestRunParams): Promise<StartTestRunResult>;
|
|
59
|
+
export interface TestScopeListing {
|
|
60
|
+
pack: string;
|
|
61
|
+
version: string;
|
|
62
|
+
scopes: {
|
|
63
|
+
id: string;
|
|
64
|
+
label: string;
|
|
65
|
+
description: string;
|
|
66
|
+
}[];
|
|
67
|
+
}
|
|
68
|
+
export declare function listTestScopes(registryClient: PackRegistryClient, cliVersion: string): Promise<TestScopeListing>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readGitMetadata } from "./git.js";
|
|
2
|
+
import { startDevXRun, DEVX_RUN_CONTRACT_VERSION } from "./devx-run.js";
|
|
3
|
+
import { fetchSecurityContext } from "./security-context.js";
|
|
4
|
+
import { resolveCompatiblePack, resolveScope, listAvailableScopes } from "./packs/resolve.js";
|
|
5
|
+
import { writeRunWorkFile } from "./run-workdir.js";
|
|
6
|
+
function scopeContentPath(resolved, scopeId) {
|
|
7
|
+
const entry = resolved.scopes.find((s) => s.id === scopeId);
|
|
8
|
+
return entry ? `${resolved.cacheDir}/${entry.path}` : resolved.cacheDir;
|
|
9
|
+
}
|
|
10
|
+
function buildAgentInstructions(resolvedScope) {
|
|
11
|
+
if (resolvedScope) {
|
|
12
|
+
return [
|
|
13
|
+
`Read the base methodology and the "${resolvedScope.label}" scope guidance at methodology.scope.path.`,
|
|
14
|
+
`Focus this review on the "${resolvedScope.label}" scope. Requested intent is scope=${resolvedScope.id} — this does not mean other areas were evaluated.`,
|
|
15
|
+
`You may report incidental findings outside this scope if you actually find them, but keep result provenance honest: do not claim coverage you did not perform.`,
|
|
16
|
+
`Use your own repository understanding, shell, and tools to perform the review. When finished, complete the run using result_contract.complete_command.`,
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
return [
|
|
20
|
+
`Read the methodology at methodology.path and perform a full, adaptive AppSec review — decide applicability yourself rather than mechanically executing every category.`,
|
|
21
|
+
`Use your own repository understanding, shell, and tools to perform the review. When finished, complete the run using result_contract.complete_command.`,
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
export async function startTestRun(deps, params) {
|
|
25
|
+
const [git, contextOutcome, resolvedPack] = await Promise.all([
|
|
26
|
+
readGitMetadata(params.cwd),
|
|
27
|
+
fetchSecurityContext(deps.apiClient, params.applicationId),
|
|
28
|
+
resolveCompatiblePack("appsec", { registryClient: deps.registryClient, cliVersion: params.cliVersion }),
|
|
29
|
+
]);
|
|
30
|
+
const scope = params.scopeId ? await resolveScope(resolvedPack, params.scopeId) : null;
|
|
31
|
+
const resolvedScope = scope ? { id: scope.id, label: scope.label } : null;
|
|
32
|
+
const started = await startDevXRun(deps.apiClient, {
|
|
33
|
+
applicationId: params.applicationId,
|
|
34
|
+
runType: "test",
|
|
35
|
+
requestedIntent: resolvedScope ? { scope: resolvedScope.id } : undefined,
|
|
36
|
+
pack: { packId: resolvedPack.packId, packType: resolvedPack.packType, version: resolvedPack.version },
|
|
37
|
+
cwd: params.cwd,
|
|
38
|
+
});
|
|
39
|
+
const contextPath = writeRunWorkFile(params.cwd, started.runId, "context.json", contextOutcome);
|
|
40
|
+
const envelope = {
|
|
41
|
+
run_id: started.runId,
|
|
42
|
+
application: { id: params.applicationId },
|
|
43
|
+
connection: { mode: "connected" },
|
|
44
|
+
git: { sha: git.commitSha ?? null, dirty: git.dirty ?? null },
|
|
45
|
+
methodology: {
|
|
46
|
+
pack: resolvedPack.packType,
|
|
47
|
+
version: resolvedPack.version,
|
|
48
|
+
path: resolvedPack.basePath,
|
|
49
|
+
...(resolvedScope ? { scope: { id: resolvedScope.id, label: resolvedScope.label, path: scopeContentPath(resolvedPack, resolvedScope.id) } } : {}),
|
|
50
|
+
},
|
|
51
|
+
context: { path: contextPath },
|
|
52
|
+
scope: resolvedScope,
|
|
53
|
+
requested_intent: resolvedScope ? { scope: resolvedScope.id } : null,
|
|
54
|
+
result_contract: {
|
|
55
|
+
schema_version: DEVX_RUN_CONTRACT_VERSION,
|
|
56
|
+
complete_command: `zivis run complete ${started.runId} --input <result.json> --json`,
|
|
57
|
+
},
|
|
58
|
+
agent_instructions: buildAgentInstructions(resolvedScope),
|
|
59
|
+
};
|
|
60
|
+
return { envelope, resolvedScope, contextOutcome };
|
|
61
|
+
}
|
|
62
|
+
export async function listTestScopes(registryClient, cliVersion) {
|
|
63
|
+
const resolvedPack = await resolveCompatiblePack("appsec", { registryClient, cliVersion });
|
|
64
|
+
return {
|
|
65
|
+
pack: resolvedPack.packType,
|
|
66
|
+
version: resolvedPack.version,
|
|
67
|
+
scopes: listAvailableScopes(resolvedPack),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ensureZivisGitignore(cwd: string): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
const MANAGED_GITIGNORE_CONTENT = `# Managed by ZIVIS — local scratch/queue state, never committed.
|
|
4
|
+
# .zivis/project.json (this directory's parent) is the one exception —
|
|
5
|
+
# it is intentionally tracked so the whole team shares the same
|
|
6
|
+
# org/Application binding.
|
|
7
|
+
extracted/
|
|
8
|
+
outbox/
|
|
9
|
+
cache/
|
|
10
|
+
work/
|
|
11
|
+
`;
|
|
12
|
+
export function ensureZivisGitignore(cwd) {
|
|
13
|
+
const zivisDir = path.join(cwd, ".zivis");
|
|
14
|
+
if (!fs.existsSync(zivisDir)) {
|
|
15
|
+
fs.mkdirSync(zivisDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
const gitignorePath = path.join(zivisDir, ".gitignore");
|
|
18
|
+
const current = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf-8") : null;
|
|
19
|
+
if (current !== MANAGED_GITIGNORE_CONTENT) {
|
|
20
|
+
fs.writeFileSync(gitignorePath, MANAGED_GITIGNORE_CONTENT);
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zivis/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.41",
|
|
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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@vscode/tree-sitter-wasm": "^0.3.1",
|
|
31
31
|
"commander": "^12.1.0",
|
|
32
32
|
"ignore": "^7.0.5",
|
|
33
|
+
"js-yaml": "^4.1.0",
|
|
33
34
|
"open": "^10.1.0",
|
|
34
35
|
"web-tree-sitter": "^0.26.8",
|
|
35
36
|
"@zivis/mcp": "0.1.5"
|
|
@@ -39,7 +40,6 @@
|
|
|
39
40
|
"@types/node": "^22.0.0",
|
|
40
41
|
"ajv": "^8.17.1",
|
|
41
42
|
"ajv-formats": "^3.0.1",
|
|
42
|
-
"js-yaml": "^4.1.0",
|
|
43
43
|
"tsx": "^4.19.2",
|
|
44
44
|
"typescript": "^5.7.0",
|
|
45
45
|
"vitest": "^3.0.0"
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build:workspace-deps": "pnpm --filter @zivis/mcp run build",
|
|
49
49
|
"build": "pnpm run build:workspace-deps && tsc -p tsconfig.build.json && node scripts/ensure-symlink.cjs",
|
|
50
|
+
"build:methodology-pack": "tsx ../scripts/build-methodology-pack.ts",
|
|
50
51
|
"dev": "tsc --watch",
|
|
51
52
|
"start": "node dist/index.js",
|
|
52
53
|
"lint": "pnpm run build:workspace-deps && tsc --noEmit",
|