@zivis/cli 0.1.0-alpha.40 → 0.1.0-alpha.42
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 +4 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { GatePolicy } from "./gate-policy.js";
|
|
2
|
+
export type CheckResult = "pass" | "fail" | "indeterminate";
|
|
3
|
+
export interface GateCheck {
|
|
4
|
+
type: string;
|
|
5
|
+
category_id?: string;
|
|
6
|
+
actual: number | string | boolean | null;
|
|
7
|
+
required: number | string | boolean;
|
|
8
|
+
result: CheckResult;
|
|
9
|
+
}
|
|
10
|
+
export interface AssuranceStatusForGate {
|
|
11
|
+
model: {
|
|
12
|
+
key: string;
|
|
13
|
+
};
|
|
14
|
+
evaluatedAt: string | null;
|
|
15
|
+
lastEvaluatedGitSha: string | null;
|
|
16
|
+
overall: {
|
|
17
|
+
scoreRaw: number | null;
|
|
18
|
+
coverage: number | null;
|
|
19
|
+
};
|
|
20
|
+
categories: Array<{
|
|
21
|
+
categoryId: string;
|
|
22
|
+
scoreRaw: number | null;
|
|
23
|
+
coverage: number | null;
|
|
24
|
+
}>;
|
|
25
|
+
risk: {
|
|
26
|
+
openCritical: number;
|
|
27
|
+
openHigh: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface MarkForGate {
|
|
31
|
+
basis: string | null;
|
|
32
|
+
expired: boolean;
|
|
33
|
+
revoked: boolean;
|
|
34
|
+
signature: {
|
|
35
|
+
verified: boolean;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface GateEvaluationInput {
|
|
39
|
+
policy: GatePolicy;
|
|
40
|
+
status: AssuranceStatusForGate | null;
|
|
41
|
+
mark: MarkForGate | null;
|
|
42
|
+
markLookupFailed: boolean;
|
|
43
|
+
currentGitSha: string | undefined;
|
|
44
|
+
now: Date;
|
|
45
|
+
}
|
|
46
|
+
export interface GateEvaluationResult {
|
|
47
|
+
result: "pass" | "fail" | "indeterminate";
|
|
48
|
+
checks: GateCheck[];
|
|
49
|
+
}
|
|
50
|
+
export declare function evaluateGate(input: GateEvaluationInput): GateEvaluationResult;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { parseDurationMs } from "./gate-policy.js";
|
|
2
|
+
export function evaluateGate(input) {
|
|
3
|
+
const checks = [];
|
|
4
|
+
const { policy, status, mark, markLookupFailed, currentGitSha, now } = input;
|
|
5
|
+
if (!status) {
|
|
6
|
+
return {
|
|
7
|
+
result: "indeterminate",
|
|
8
|
+
checks: [{ type: "assurance_status", actual: null, required: "present", result: "indeterminate" }],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
if (policy.assurance?.overall?.min_score_raw !== undefined) {
|
|
12
|
+
checks.push(scoreCheck("overall_score", undefined, status.overall.scoreRaw, policy.assurance.overall.min_score_raw));
|
|
13
|
+
}
|
|
14
|
+
if (policy.assurance?.overall?.min_coverage !== undefined) {
|
|
15
|
+
checks.push(scoreCheck("overall_coverage", undefined, status.overall.coverage, policy.assurance.overall.min_coverage));
|
|
16
|
+
}
|
|
17
|
+
for (const cat of policy.assurance?.categories ?? []) {
|
|
18
|
+
const found = status.categories.find((c) => c.categoryId === cat.id);
|
|
19
|
+
if (cat.min_score_raw !== undefined) {
|
|
20
|
+
checks.push(scoreCheck("category_score", cat.id, found ? found.scoreRaw : null, cat.min_score_raw));
|
|
21
|
+
}
|
|
22
|
+
if (cat.min_coverage !== undefined) {
|
|
23
|
+
checks.push(scoreCheck("category_coverage", cat.id, found ? found.coverage : null, cat.min_coverage));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (policy.risk?.max_open_critical !== undefined) {
|
|
27
|
+
checks.push(countCeilingCheck("open_critical_risks", status.risk.openCritical, policy.risk.max_open_critical));
|
|
28
|
+
}
|
|
29
|
+
if (policy.risk?.max_open_high !== undefined) {
|
|
30
|
+
checks.push(countCeilingCheck("open_high_risks", status.risk.openHigh, policy.risk.max_open_high));
|
|
31
|
+
}
|
|
32
|
+
if (policy.freshness?.max_age !== undefined) {
|
|
33
|
+
checks.push(freshnessAgeCheck(status.evaluatedAt, policy.freshness.max_age, now));
|
|
34
|
+
}
|
|
35
|
+
if (policy.freshness?.require_current_git_sha) {
|
|
36
|
+
checks.push(freshnessGitShaCheck(status.lastEvaluatedGitSha, currentGitSha));
|
|
37
|
+
}
|
|
38
|
+
if (policy.provenance?.allowed_basis !== undefined) {
|
|
39
|
+
checks.push(provenanceBasisCheck(mark, markLookupFailed, policy.provenance.allowed_basis));
|
|
40
|
+
}
|
|
41
|
+
if (policy.provenance?.require_signed_mark) {
|
|
42
|
+
checks.push(provenanceSignedMarkCheck(mark, markLookupFailed));
|
|
43
|
+
}
|
|
44
|
+
const overall = combine(checks);
|
|
45
|
+
return { result: overall, checks };
|
|
46
|
+
}
|
|
47
|
+
function combine(checks) {
|
|
48
|
+
if (checks.length === 0)
|
|
49
|
+
return "pass";
|
|
50
|
+
if (checks.some((c) => c.result === "indeterminate"))
|
|
51
|
+
return "indeterminate";
|
|
52
|
+
if (checks.some((c) => c.result === "fail"))
|
|
53
|
+
return "fail";
|
|
54
|
+
return "pass";
|
|
55
|
+
}
|
|
56
|
+
function scoreCheck(type, categoryId, actual, required) {
|
|
57
|
+
return {
|
|
58
|
+
type,
|
|
59
|
+
...(categoryId ? { category_id: categoryId } : {}),
|
|
60
|
+
actual,
|
|
61
|
+
required,
|
|
62
|
+
result: actual === null ? "indeterminate" : actual >= required ? "pass" : "fail",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function countCeilingCheck(type, actual, maxAllowed) {
|
|
66
|
+
return { type, actual, required: maxAllowed, result: actual <= maxAllowed ? "pass" : "fail" };
|
|
67
|
+
}
|
|
68
|
+
function freshnessAgeCheck(evaluatedAt, maxAge, now) {
|
|
69
|
+
if (!evaluatedAt) {
|
|
70
|
+
return { type: "freshness_max_age", actual: null, required: maxAge, result: "indeterminate" };
|
|
71
|
+
}
|
|
72
|
+
const maxAgeMs = parseDurationMs(maxAge);
|
|
73
|
+
if (maxAgeMs === null) {
|
|
74
|
+
return { type: "freshness_max_age", actual: evaluatedAt, required: maxAge, result: "indeterminate" };
|
|
75
|
+
}
|
|
76
|
+
const ageMs = now.getTime() - new Date(evaluatedAt).getTime();
|
|
77
|
+
return { type: "freshness_max_age", actual: `${Math.round(ageMs / 1000)}s`, required: maxAge, result: ageMs <= maxAgeMs ? "pass" : "indeterminate" };
|
|
78
|
+
}
|
|
79
|
+
function freshnessGitShaCheck(lastEvaluatedGitSha, currentGitSha) {
|
|
80
|
+
const matches = !!lastEvaluatedGitSha && !!currentGitSha && lastEvaluatedGitSha === currentGitSha;
|
|
81
|
+
return {
|
|
82
|
+
type: "freshness_current_git_sha",
|
|
83
|
+
actual: lastEvaluatedGitSha,
|
|
84
|
+
required: currentGitSha ?? "HEAD",
|
|
85
|
+
result: matches ? "pass" : "indeterminate",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function provenanceBasisCheck(mark, markLookupFailed, allowedBasis) {
|
|
89
|
+
if (markLookupFailed) {
|
|
90
|
+
return { type: "provenance_basis", actual: null, required: allowedBasis.join(","), result: "indeterminate" };
|
|
91
|
+
}
|
|
92
|
+
if (!mark || !mark.basis) {
|
|
93
|
+
return { type: "provenance_basis", actual: null, required: allowedBasis.join(","), result: "fail" };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
type: "provenance_basis",
|
|
97
|
+
actual: mark.basis,
|
|
98
|
+
required: allowedBasis.join(","),
|
|
99
|
+
result: allowedBasis.includes(mark.basis) ? "pass" : "fail",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function provenanceSignedMarkCheck(mark, markLookupFailed) {
|
|
103
|
+
if (markLookupFailed) {
|
|
104
|
+
return { type: "provenance_signed_mark", actual: null, required: true, result: "indeterminate" };
|
|
105
|
+
}
|
|
106
|
+
if (!mark) {
|
|
107
|
+
return { type: "provenance_signed_mark", actual: false, required: true, result: "fail" };
|
|
108
|
+
}
|
|
109
|
+
const valid = mark.signature.verified && !mark.expired && !mark.revoked;
|
|
110
|
+
return { type: "provenance_signed_mark", actual: valid, required: true, result: valid ? "pass" : "fail" };
|
|
111
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface CategoryThreshold {
|
|
2
|
+
id: string;
|
|
3
|
+
min_score_raw?: number;
|
|
4
|
+
min_coverage?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface GatePolicy {
|
|
7
|
+
source: "current";
|
|
8
|
+
model: string;
|
|
9
|
+
scoring_profile?: string;
|
|
10
|
+
assurance?: {
|
|
11
|
+
overall?: {
|
|
12
|
+
min_score_raw?: number;
|
|
13
|
+
min_coverage?: number;
|
|
14
|
+
};
|
|
15
|
+
categories?: CategoryThreshold[];
|
|
16
|
+
};
|
|
17
|
+
risk?: {
|
|
18
|
+
max_open_critical?: number;
|
|
19
|
+
max_open_high?: number;
|
|
20
|
+
};
|
|
21
|
+
freshness?: {
|
|
22
|
+
max_age?: string;
|
|
23
|
+
require_current_git_sha?: boolean;
|
|
24
|
+
};
|
|
25
|
+
provenance?: {
|
|
26
|
+
allowed_basis?: string[];
|
|
27
|
+
require_signed_mark?: boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface PolicyFile {
|
|
31
|
+
version: 1;
|
|
32
|
+
gates: Record<string, GatePolicy>;
|
|
33
|
+
}
|
|
34
|
+
export declare class PolicyValidationError extends Error {
|
|
35
|
+
constructor(message: string);
|
|
36
|
+
}
|
|
37
|
+
export declare function parsePolicyFile(rawYaml: string): PolicyFile;
|
|
38
|
+
export declare function parseDurationMs(spec: string): number | null;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as yaml from "js-yaml";
|
|
2
|
+
export class PolicyValidationError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "PolicyValidationError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const TOP_LEVEL_KEYS = new Set(["version", "gates"]);
|
|
9
|
+
const GATE_KEYS = new Set(["source", "model", "scoring_profile", "assurance", "risk", "freshness", "provenance"]);
|
|
10
|
+
const ASSURANCE_KEYS = new Set(["overall", "categories"]);
|
|
11
|
+
const SCORE_KEYS = new Set(["min_score_raw", "min_coverage"]);
|
|
12
|
+
const CATEGORY_KEYS = new Set(["id", "min_score_raw", "min_coverage"]);
|
|
13
|
+
const RISK_KEYS = new Set(["max_open_critical", "max_open_high"]);
|
|
14
|
+
const FRESHNESS_KEYS = new Set(["max_age", "require_current_git_sha"]);
|
|
15
|
+
const PROVENANCE_KEYS = new Set(["allowed_basis", "require_signed_mark"]);
|
|
16
|
+
function rejectUnknown(obj, allowed, where) {
|
|
17
|
+
for (const key of Object.keys(obj)) {
|
|
18
|
+
if (!allowed.has(key)) {
|
|
19
|
+
throw new PolicyValidationError(`Unknown policy field '${where}.${key}' — v1 does not recognize this key.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function isPlainObject(v) {
|
|
24
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
25
|
+
}
|
|
26
|
+
export function parsePolicyFile(rawYaml) {
|
|
27
|
+
let doc;
|
|
28
|
+
try {
|
|
29
|
+
doc = yaml.load(rawYaml);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
throw new PolicyValidationError(`Could not parse policy YAML: ${err instanceof Error ? err.message : String(err)}`);
|
|
33
|
+
}
|
|
34
|
+
if (!isPlainObject(doc)) {
|
|
35
|
+
throw new PolicyValidationError("Policy file must be a YAML mapping at the top level.");
|
|
36
|
+
}
|
|
37
|
+
rejectUnknown(doc, TOP_LEVEL_KEYS, "$");
|
|
38
|
+
if (doc.version !== 1) {
|
|
39
|
+
throw new PolicyValidationError(`Unsupported policy 'version': ${JSON.stringify(doc.version)} — only version 1 is recognized.`);
|
|
40
|
+
}
|
|
41
|
+
if (!isPlainObject(doc.gates)) {
|
|
42
|
+
throw new PolicyValidationError("Policy file must declare a 'gates' mapping.");
|
|
43
|
+
}
|
|
44
|
+
const gates = {};
|
|
45
|
+
for (const [name, raw] of Object.entries(doc.gates)) {
|
|
46
|
+
gates[name] = parseGate(name, raw);
|
|
47
|
+
}
|
|
48
|
+
return { version: 1, gates };
|
|
49
|
+
}
|
|
50
|
+
function parseGate(name, raw) {
|
|
51
|
+
if (!isPlainObject(raw)) {
|
|
52
|
+
throw new PolicyValidationError(`Gate '${name}' must be a YAML mapping.`);
|
|
53
|
+
}
|
|
54
|
+
rejectUnknown(raw, GATE_KEYS, `gates.${name}`);
|
|
55
|
+
if (raw.source !== "current") {
|
|
56
|
+
throw new PolicyValidationError(`Gate '${name}.source' must be 'current' — only live current-state gates are supported in v1.`);
|
|
57
|
+
}
|
|
58
|
+
if (typeof raw.model !== "string" || !raw.model) {
|
|
59
|
+
throw new PolicyValidationError(`Gate '${name}.model' is required and must be a string.`);
|
|
60
|
+
}
|
|
61
|
+
if (raw.scoring_profile !== undefined && typeof raw.scoring_profile !== "string") {
|
|
62
|
+
throw new PolicyValidationError(`Gate '${name}.scoring_profile' must be a string.`);
|
|
63
|
+
}
|
|
64
|
+
const policy = { source: "current", model: raw.model };
|
|
65
|
+
if (typeof raw.scoring_profile === "string")
|
|
66
|
+
policy.scoring_profile = raw.scoring_profile;
|
|
67
|
+
if (raw.assurance !== undefined) {
|
|
68
|
+
if (!isPlainObject(raw.assurance))
|
|
69
|
+
throw new PolicyValidationError(`Gate '${name}.assurance' must be a mapping.`);
|
|
70
|
+
rejectUnknown(raw.assurance, ASSURANCE_KEYS, `gates.${name}.assurance`);
|
|
71
|
+
policy.assurance = {};
|
|
72
|
+
if (raw.assurance.overall !== undefined) {
|
|
73
|
+
if (!isPlainObject(raw.assurance.overall))
|
|
74
|
+
throw new PolicyValidationError(`Gate '${name}.assurance.overall' must be a mapping.`);
|
|
75
|
+
rejectUnknown(raw.assurance.overall, SCORE_KEYS, `gates.${name}.assurance.overall`);
|
|
76
|
+
policy.assurance.overall = parseScoreThreshold(raw.assurance.overall, `${name}.assurance.overall`);
|
|
77
|
+
}
|
|
78
|
+
if (raw.assurance.categories !== undefined) {
|
|
79
|
+
if (!Array.isArray(raw.assurance.categories))
|
|
80
|
+
throw new PolicyValidationError(`Gate '${name}.assurance.categories' must be a list.`);
|
|
81
|
+
policy.assurance.categories = raw.assurance.categories.map((c, i) => {
|
|
82
|
+
if (!isPlainObject(c))
|
|
83
|
+
throw new PolicyValidationError(`Gate '${name}.assurance.categories[${i}]' must be a mapping.`);
|
|
84
|
+
rejectUnknown(c, CATEGORY_KEYS, `gates.${name}.assurance.categories[${i}]`);
|
|
85
|
+
if (typeof c.id !== "string" || !c.id) {
|
|
86
|
+
throw new PolicyValidationError(`Gate '${name}.assurance.categories[${i}].id' is required and must be a string.`);
|
|
87
|
+
}
|
|
88
|
+
return { id: c.id, ...parseScoreThreshold(c, `${name}.assurance.categories[${i}]`) };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (raw.risk !== undefined) {
|
|
93
|
+
if (!isPlainObject(raw.risk))
|
|
94
|
+
throw new PolicyValidationError(`Gate '${name}.risk' must be a mapping.`);
|
|
95
|
+
rejectUnknown(raw.risk, RISK_KEYS, `gates.${name}.risk`);
|
|
96
|
+
policy.risk = {};
|
|
97
|
+
if (raw.risk.max_open_critical !== undefined)
|
|
98
|
+
policy.risk.max_open_critical = requireNonNegativeInt(raw.risk.max_open_critical, `${name}.risk.max_open_critical`);
|
|
99
|
+
if (raw.risk.max_open_high !== undefined)
|
|
100
|
+
policy.risk.max_open_high = requireNonNegativeInt(raw.risk.max_open_high, `${name}.risk.max_open_high`);
|
|
101
|
+
}
|
|
102
|
+
if (raw.freshness !== undefined) {
|
|
103
|
+
if (!isPlainObject(raw.freshness))
|
|
104
|
+
throw new PolicyValidationError(`Gate '${name}.freshness' must be a mapping.`);
|
|
105
|
+
rejectUnknown(raw.freshness, FRESHNESS_KEYS, `gates.${name}.freshness`);
|
|
106
|
+
policy.freshness = {};
|
|
107
|
+
if (raw.freshness.max_age !== undefined) {
|
|
108
|
+
if (typeof raw.freshness.max_age !== "string" || parseDurationMs(raw.freshness.max_age) === null) {
|
|
109
|
+
throw new PolicyValidationError(`Gate '${name}.freshness.max_age' must be a duration like '14d', '24h', or '30m'.`);
|
|
110
|
+
}
|
|
111
|
+
policy.freshness.max_age = raw.freshness.max_age;
|
|
112
|
+
}
|
|
113
|
+
if (raw.freshness.require_current_git_sha !== undefined) {
|
|
114
|
+
if (typeof raw.freshness.require_current_git_sha !== "boolean") {
|
|
115
|
+
throw new PolicyValidationError(`Gate '${name}.freshness.require_current_git_sha' must be true or false.`);
|
|
116
|
+
}
|
|
117
|
+
policy.freshness.require_current_git_sha = raw.freshness.require_current_git_sha;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (raw.provenance !== undefined) {
|
|
121
|
+
if (!isPlainObject(raw.provenance))
|
|
122
|
+
throw new PolicyValidationError(`Gate '${name}.provenance' must be a mapping.`);
|
|
123
|
+
rejectUnknown(raw.provenance, PROVENANCE_KEYS, `gates.${name}.provenance`);
|
|
124
|
+
policy.provenance = {};
|
|
125
|
+
if (raw.provenance.allowed_basis !== undefined) {
|
|
126
|
+
if (!Array.isArray(raw.provenance.allowed_basis) || !raw.provenance.allowed_basis.every((b) => typeof b === "string")) {
|
|
127
|
+
throw new PolicyValidationError(`Gate '${name}.provenance.allowed_basis' must be a list of strings.`);
|
|
128
|
+
}
|
|
129
|
+
policy.provenance.allowed_basis = raw.provenance.allowed_basis;
|
|
130
|
+
}
|
|
131
|
+
if (raw.provenance.require_signed_mark !== undefined) {
|
|
132
|
+
if (typeof raw.provenance.require_signed_mark !== "boolean") {
|
|
133
|
+
throw new PolicyValidationError(`Gate '${name}.provenance.require_signed_mark' must be true or false.`);
|
|
134
|
+
}
|
|
135
|
+
policy.provenance.require_signed_mark = raw.provenance.require_signed_mark;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return policy;
|
|
139
|
+
}
|
|
140
|
+
function parseScoreThreshold(obj, where) {
|
|
141
|
+
const out = {};
|
|
142
|
+
if (obj.min_score_raw !== undefined)
|
|
143
|
+
out.min_score_raw = requireUnitInterval(obj.min_score_raw, `${where}.min_score_raw`);
|
|
144
|
+
if (obj.min_coverage !== undefined)
|
|
145
|
+
out.min_coverage = requireUnitInterval(obj.min_coverage, `${where}.min_coverage`);
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function requireUnitInterval(v, where) {
|
|
149
|
+
if (typeof v !== "number" || Number.isNaN(v) || v < 0 || v > 1) {
|
|
150
|
+
throw new PolicyValidationError(`'${where}' must be a number between 0.0 and 1.0 (normalized score_raw — never 0-10 or 0-1000).`);
|
|
151
|
+
}
|
|
152
|
+
return v;
|
|
153
|
+
}
|
|
154
|
+
function requireNonNegativeInt(v, where) {
|
|
155
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0) {
|
|
156
|
+
throw new PolicyValidationError(`'${where}' must be a non-negative integer.`);
|
|
157
|
+
}
|
|
158
|
+
return v;
|
|
159
|
+
}
|
|
160
|
+
const DURATION_RE = /^(\d+)(d|h|m)$/;
|
|
161
|
+
const DURATION_UNIT_MS = { d: 86_400_000, h: 3_600_000, m: 60_000 };
|
|
162
|
+
export function parseDurationMs(spec) {
|
|
163
|
+
const m = DURATION_RE.exec(spec.trim());
|
|
164
|
+
if (!m)
|
|
165
|
+
return null;
|
|
166
|
+
return Number(m[1]) * DURATION_UNIT_MS[m[2]];
|
|
167
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface GitMetadata {
|
|
2
|
+
remoteUrl?: string;
|
|
3
|
+
commitSha?: string;
|
|
4
|
+
branch?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function readGitMetadata(rootDir: string): Promise<GitMetadata>;
|
|
7
|
+
export declare function deriveRepoFullNameFromRemote(remoteUrl: string | undefined): string | undefined;
|
|
8
|
+
export declare function deriveRepoNameFromGit(remoteUrl: string | undefined, rootDir: string): string | undefined;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
export async function readGitMetadata(rootDir) {
|
|
6
|
+
return {
|
|
7
|
+
remoteUrl: await tryGit(rootDir, "remote get-url origin"),
|
|
8
|
+
commitSha: await tryGit(rootDir, "rev-parse HEAD"),
|
|
9
|
+
branch: await tryGit(rootDir, "rev-parse --abbrev-ref HEAD"),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
async function tryGit(cwd, args) {
|
|
13
|
+
try {
|
|
14
|
+
const { stdout } = await execAsync(`git ${args}`, { cwd });
|
|
15
|
+
const value = stdout.trim();
|
|
16
|
+
return value || undefined;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function deriveRepoFullNameFromRemote(remoteUrl) {
|
|
23
|
+
if (!remoteUrl)
|
|
24
|
+
return undefined;
|
|
25
|
+
const m = /[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/.exec(remoteUrl);
|
|
26
|
+
if (!m)
|
|
27
|
+
return undefined;
|
|
28
|
+
return `${m[1]}/${m[2]}`;
|
|
29
|
+
}
|
|
30
|
+
export function deriveRepoNameFromGit(remoteUrl, rootDir) {
|
|
31
|
+
if (remoteUrl) {
|
|
32
|
+
const m = /[/:]([^/:]+?)\/([^/]+?)(?:\.git)?$/.exec(remoteUrl);
|
|
33
|
+
if (m)
|
|
34
|
+
return m[2];
|
|
35
|
+
}
|
|
36
|
+
const base = path.basename(rootDir);
|
|
37
|
+
return base && base !== "." ? base : undefined;
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface GitMetadata {
|
|
2
|
+
remoteUrl?: string;
|
|
3
|
+
commitSha?: string;
|
|
4
|
+
branch?: string;
|
|
5
|
+
dirty?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function readGitMetadata(rootDir?: string): Promise<GitMetadata>;
|
|
8
|
+
export declare function getCurrentGitSha(rootDir?: string): Promise<string | undefined>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
export async function readGitMetadata(rootDir = process.cwd()) {
|
|
5
|
+
const [remoteUrl, commitSha, branch, statusOutput] = await Promise.all([
|
|
6
|
+
tryGit(rootDir, "remote get-url origin"),
|
|
7
|
+
tryGit(rootDir, "rev-parse HEAD"),
|
|
8
|
+
tryGit(rootDir, "rev-parse --abbrev-ref HEAD"),
|
|
9
|
+
tryGit(rootDir, "status --porcelain"),
|
|
10
|
+
]);
|
|
11
|
+
return {
|
|
12
|
+
remoteUrl,
|
|
13
|
+
commitSha,
|
|
14
|
+
branch,
|
|
15
|
+
dirty: statusOutput === undefined ? undefined : statusOutput.length > 0,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export async function getCurrentGitSha(rootDir = process.cwd()) {
|
|
19
|
+
return tryGit(rootDir, "rev-parse HEAD");
|
|
20
|
+
}
|
|
21
|
+
async function tryGit(cwd, args) {
|
|
22
|
+
try {
|
|
23
|
+
const { stdout } = await execAsync(`git ${args}`, { cwd });
|
|
24
|
+
const value = stdout.trim();
|
|
25
|
+
return value || undefined;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -5,6 +5,7 @@ export interface GuidanceOptions {
|
|
|
5
5
|
client: GuidanceClient;
|
|
6
6
|
}
|
|
7
7
|
export declare function writeGuidance(opts: GuidanceOptions): string[];
|
|
8
|
+
export declare function removeGuidance(opts: GuidanceOptions): string[];
|
|
8
9
|
export declare function detectIdesInRepo(cwd: string): GuidanceClient[];
|
|
9
10
|
export declare function setupCursor(pkg?: Package): Promise<void>;
|
|
10
11
|
export declare function setupCursorWorkspace(pkg?: Package): Promise<void>;
|
|
@@ -27,6 +27,19 @@ function appendBlock(filePath, blockKey, content) {
|
|
|
27
27
|
function escapeRegex(s) {
|
|
28
28
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
29
29
|
}
|
|
30
|
+
function removeBlockFrom(filePath, blockKey) {
|
|
31
|
+
if (!fs.existsSync(filePath))
|
|
32
|
+
return false;
|
|
33
|
+
const begin = `<!-- BEGIN ZIVIS:${blockKey} -->`;
|
|
34
|
+
const end = `<!-- END ZIVIS:${blockKey} -->`;
|
|
35
|
+
const blockRegex = new RegExp(`\\n?${escapeRegex(begin)}[\\s\\S]*?${escapeRegex(end)}\\n?`, "g");
|
|
36
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
37
|
+
if (!blockRegex.test(existing))
|
|
38
|
+
return false;
|
|
39
|
+
const stripped = existing.replace(blockRegex, "\n").replace(/\n{3,}/g, "\n\n");
|
|
40
|
+
fs.writeFileSync(filePath, stripped);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
30
43
|
function cursorRuleContent() {
|
|
31
44
|
return `---
|
|
32
45
|
description: ZIVIS security guidance — quietly suggest security checks at the right moments
|
|
@@ -112,21 +125,14 @@ might break in", "security plan", "categories of attacks".
|
|
|
112
125
|
<!-- END ZIVIS:guidance -->`;
|
|
113
126
|
}
|
|
114
127
|
function claudeBlockContent() {
|
|
115
|
-
return `## ZIVIS
|
|
116
|
-
|
|
117
|
-
ZIVIS is connected to this project. Use its tools sparingly — only for security-related topics.
|
|
128
|
+
return `## ZIVIS
|
|
118
129
|
|
|
119
|
-
|
|
120
|
-
- User asks "is this secure" / "check for vulnerabilities" / "audit my deps" → call \`zivis_get_started\` first
|
|
121
|
-
- User adds a new npm/pip/go package → offer to call \`zivis_check_repo_trust\` (no auth needed)
|
|
122
|
-
- User adds openai/anthropic/langchain import → offer to call \`zivis_setup_red_team_target\` (target_type=ai_chat)
|
|
123
|
-
- User mentions "launch", "audit", "compliance", "pen test" → call \`zivis_get_started\` with concern=<their phrase>
|
|
130
|
+
ZIVIS is connected to this project via the \`zivis\` CLI — one agent-neutral command surface, not IDE-specific tooling.
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
**Slash commands:** /zivis-getting-started /zivis-secure-my-app /zivis-audit-dependencies /zivis-prep-for-launch`;
|
|
132
|
+
- Asked to security test or review this repo → run \`zivis test\` (or \`zivis test <scope>\` for a focused pass; \`zivis test --list\` shows available scopes) and follow the guidance it returns.
|
|
133
|
+
- Asked to threat model this repo → run \`zivis threatmodel\` and follow the guidance it returns.
|
|
134
|
+
- Use your own repo understanding, reasoning, and shell/tools/browser access as needed — these commands are a starting point, not a substitute for judgment.
|
|
135
|
+
- Do not treat their output alone as a confirmed finding; verify before reporting.`;
|
|
130
136
|
}
|
|
131
137
|
function ruleFileContent() {
|
|
132
138
|
return `# ZIVIS Security Guidance
|
|
@@ -191,10 +197,8 @@ function writeClaudeBlockGuidance(cwd) {
|
|
|
191
197
|
appendBlock(claudePath, "zivis-security", claudeBlockContent());
|
|
192
198
|
written.push(claudePath);
|
|
193
199
|
const agentsPath = path.join(cwd, "AGENTS.md");
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
written.push(agentsPath);
|
|
197
|
-
}
|
|
200
|
+
appendBlock(agentsPath, "zivis-security", claudeBlockContent());
|
|
201
|
+
written.push(agentsPath);
|
|
198
202
|
return written;
|
|
199
203
|
}
|
|
200
204
|
function writeCopilotGuidance(cwd) {
|
|
@@ -221,8 +225,14 @@ export function writeGuidance(opts) {
|
|
|
221
225
|
switch (client) {
|
|
222
226
|
case "cursor": {
|
|
223
227
|
written.push(writeCursorGuidance(cwd));
|
|
224
|
-
const
|
|
225
|
-
|
|
228
|
+
const claudePath = path.join(cwd, "CLAUDE.md");
|
|
229
|
+
appendBlock(claudePath, "zivis-security", claudeBlockContent());
|
|
230
|
+
written.push(claudePath);
|
|
231
|
+
const agentsPath = path.join(cwd, "AGENTS.md");
|
|
232
|
+
if (fs.existsSync(agentsPath)) {
|
|
233
|
+
appendBlock(agentsPath, "zivis-security", claudeBlockContent());
|
|
234
|
+
written.push(agentsPath);
|
|
235
|
+
}
|
|
226
236
|
break;
|
|
227
237
|
}
|
|
228
238
|
case "claude-code-cli":
|
|
@@ -245,6 +255,53 @@ export function writeGuidance(opts) {
|
|
|
245
255
|
}
|
|
246
256
|
return written.filter(Boolean);
|
|
247
257
|
}
|
|
258
|
+
export function removeGuidance(opts) {
|
|
259
|
+
const { cwd, client } = opts;
|
|
260
|
+
const changed = [];
|
|
261
|
+
const unlinkIfExists = (p) => {
|
|
262
|
+
if (fs.existsSync(p)) {
|
|
263
|
+
fs.unlinkSync(p);
|
|
264
|
+
changed.push(p);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
switch (client) {
|
|
268
|
+
case "cursor": {
|
|
269
|
+
unlinkIfExists(path.join(cwd, ".cursor", "rules", "zivis.mdc"));
|
|
270
|
+
if (removeBlockFrom(path.join(cwd, "CLAUDE.md"), "zivis-security")) {
|
|
271
|
+
changed.push(path.join(cwd, "CLAUDE.md"));
|
|
272
|
+
}
|
|
273
|
+
if (removeBlockFrom(path.join(cwd, "AGENTS.md"), "zivis-security")) {
|
|
274
|
+
changed.push(path.join(cwd, "AGENTS.md"));
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case "claude-code-cli":
|
|
279
|
+
case "vscode-claude": {
|
|
280
|
+
if (removeBlockFrom(path.join(cwd, "CLAUDE.md"), "zivis-security")) {
|
|
281
|
+
changed.push(path.join(cwd, "CLAUDE.md"));
|
|
282
|
+
}
|
|
283
|
+
if (removeBlockFrom(path.join(cwd, "AGENTS.md"), "zivis-security")) {
|
|
284
|
+
changed.push(path.join(cwd, "AGENTS.md"));
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
case "vscode-copilot": {
|
|
289
|
+
if (removeBlockFrom(path.join(cwd, ".github", "copilot-instructions.md"), "zivis-security")) {
|
|
290
|
+
changed.push(path.join(cwd, ".github", "copilot-instructions.md"));
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case "cline": {
|
|
295
|
+
unlinkIfExists(path.join(cwd, ".clinerules"));
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case "windsurf": {
|
|
299
|
+
unlinkIfExists(path.join(cwd, ".windsurfrules"));
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return changed;
|
|
304
|
+
}
|
|
248
305
|
export function detectIdesInRepo(cwd) {
|
|
249
306
|
const found = [];
|
|
250
307
|
if (fs.existsSync(path.join(cwd, ".cursor"))) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface InventorySyncEndpoint {
|
|
2
|
+
path: string;
|
|
3
|
+
method: string;
|
|
4
|
+
summary?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
authRequired?: boolean;
|
|
7
|
+
tags?: string[];
|
|
8
|
+
deprecated?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface InventorySyncFeature {
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
criticality?: "critical" | "high" | "medium" | "low";
|
|
14
|
+
endpoints?: string[];
|
|
15
|
+
tags?: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface InventorySyncInput {
|
|
18
|
+
endpoints?: InventorySyncEndpoint[];
|
|
19
|
+
features?: InventorySyncFeature[];
|
|
20
|
+
technologyStack?: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface InventorySyncProvenance {
|
|
23
|
+
gitCommitSha?: string | null;
|
|
24
|
+
repoFullName?: string | null;
|
|
25
|
+
defaultBranch?: string | null;
|
|
26
|
+
source: "cli" | "agent";
|
|
27
|
+
}
|
|
28
|
+
export type ValidationResult = {
|
|
29
|
+
ok: true;
|
|
30
|
+
value: InventorySyncInput;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
errors: string[];
|
|
34
|
+
};
|
|
35
|
+
export declare function validateInventorySyncInput(raw: unknown): ValidationResult;
|
|
36
|
+
export interface EndpointReconcileResult {
|
|
37
|
+
created: number;
|
|
38
|
+
updated: number;
|
|
39
|
+
deprecated: number;
|
|
40
|
+
total: number;
|
|
41
|
+
}
|
|
42
|
+
export interface FeatureReconcileResult {
|
|
43
|
+
created: number;
|
|
44
|
+
updated: number;
|
|
45
|
+
deprecated: number;
|
|
46
|
+
endpointsLinked: number;
|
|
47
|
+
filesLinked: number;
|
|
48
|
+
}
|
|
49
|
+
export interface TechStackResult {
|
|
50
|
+
id: string;
|
|
51
|
+
techStack: string[];
|
|
52
|
+
}
|
|
53
|
+
export interface RepoSyncResult {
|
|
54
|
+
created: boolean;
|
|
55
|
+
repoFullName: string;
|
|
56
|
+
}
|
|
57
|
+
export interface InventorySyncSummary {
|
|
58
|
+
endpoints?: EndpointReconcileResult;
|
|
59
|
+
features?: FeatureReconcileResult;
|
|
60
|
+
technologyStack?: {
|
|
61
|
+
count: number;
|
|
62
|
+
};
|
|
63
|
+
repo?: RepoSyncResult;
|
|
64
|
+
}
|
|
65
|
+
export interface InventorySyncApi {
|
|
66
|
+
put<T>(path: string, body: unknown): Promise<T>;
|
|
67
|
+
post<T>(path: string, body: unknown): Promise<T>;
|
|
68
|
+
}
|
|
69
|
+
export declare function syncApplicationInventory(params: {
|
|
70
|
+
client: InventorySyncApi;
|
|
71
|
+
applicationId: string;
|
|
72
|
+
input: InventorySyncInput;
|
|
73
|
+
provenance: InventorySyncProvenance;
|
|
74
|
+
}): Promise<InventorySyncSummary>;
|