nexarch 0.6.7 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init-project.js +153 -38
- package/dist/commands/list-entities.js +46 -0
- package/dist/commands/list-relationships.js +49 -0
- package/dist/commands/policy-audit-submit.js +31 -5
- package/dist/commands/policy-audit-template.js +95 -0
- package/dist/commands/status.js +11 -1
- package/dist/index.js +28 -0
- package/package.json +1 -1
|
@@ -207,6 +207,63 @@ function parseEnvKeys(content) {
|
|
|
207
207
|
.filter((key) => /^[A-Z][A-Z0-9_]{2,}$/.test(key))
|
|
208
208
|
.filter((key) => !ENV_KEY_NOISE.test(key));
|
|
209
209
|
}
|
|
210
|
+
function inferEnvServiceNamesFromKey(key) {
|
|
211
|
+
const upper = key.toUpperCase();
|
|
212
|
+
const names = new Set();
|
|
213
|
+
if (upper.includes("ADSENSE")) {
|
|
214
|
+
names.add("google-adsense");
|
|
215
|
+
names.add("adsense");
|
|
216
|
+
}
|
|
217
|
+
if (upper.includes("VERCEL"))
|
|
218
|
+
names.add("vercel");
|
|
219
|
+
if (upper.includes("NEON"))
|
|
220
|
+
names.add("neon");
|
|
221
|
+
if (upper.includes("TURBO")) {
|
|
222
|
+
names.add("turbo");
|
|
223
|
+
names.add("turborepo");
|
|
224
|
+
}
|
|
225
|
+
return Array.from(names);
|
|
226
|
+
}
|
|
227
|
+
function inferEnvServiceNamesFromValue(valueRaw) {
|
|
228
|
+
const value = valueRaw.replace(/^['"]|['"]$/g, "").toLowerCase();
|
|
229
|
+
const names = new Set();
|
|
230
|
+
if (!value)
|
|
231
|
+
return [];
|
|
232
|
+
if (value.includes("neon.tech") || value.includes("@neondatabase/"))
|
|
233
|
+
names.add("neon");
|
|
234
|
+
if (value.includes("vercel.app") || value.includes("vercel.com"))
|
|
235
|
+
names.add("vercel");
|
|
236
|
+
if (value.includes("googleads.g.doubleclick.net") || value.includes("adsense")) {
|
|
237
|
+
names.add("google-adsense");
|
|
238
|
+
names.add("adsense");
|
|
239
|
+
}
|
|
240
|
+
return Array.from(names);
|
|
241
|
+
}
|
|
242
|
+
function parseEnvSignals(content) {
|
|
243
|
+
const names = new Set();
|
|
244
|
+
const keys = parseEnvKeys(content);
|
|
245
|
+
for (const key of keys) {
|
|
246
|
+
names.add(key);
|
|
247
|
+
for (const n of inferEnvServiceNamesFromKey(key))
|
|
248
|
+
names.add(n);
|
|
249
|
+
}
|
|
250
|
+
for (const rawLine of content.split("\n")) {
|
|
251
|
+
const line = rawLine.trim();
|
|
252
|
+
if (!line || line.startsWith("#") || !line.includes("="))
|
|
253
|
+
continue;
|
|
254
|
+
const normalized = line.replace(/^export\s+/, "");
|
|
255
|
+
const eqIdx = normalized.indexOf("=");
|
|
256
|
+
if (eqIdx < 1)
|
|
257
|
+
continue;
|
|
258
|
+
const key = normalized.slice(0, eqIdx).trim();
|
|
259
|
+
const value = normalized.slice(eqIdx + 1).trim();
|
|
260
|
+
if (!/^[A-Z][A-Z0-9_]{2,}$/.test(key))
|
|
261
|
+
continue;
|
|
262
|
+
for (const n of inferEnvServiceNamesFromValue(value))
|
|
263
|
+
names.add(n);
|
|
264
|
+
}
|
|
265
|
+
return Array.from(names);
|
|
266
|
+
}
|
|
210
267
|
function gitOriginUrl(dir) {
|
|
211
268
|
const configPath = join(dir, ".git", "config");
|
|
212
269
|
if (!existsSync(configPath))
|
|
@@ -237,38 +294,80 @@ function detectFromGitConfig(dir) {
|
|
|
237
294
|
return [];
|
|
238
295
|
}
|
|
239
296
|
function detectFromFilesystem(dir) {
|
|
240
|
-
const detected =
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
297
|
+
const detected = new Set();
|
|
298
|
+
const markIfExists = (relPath, name) => {
|
|
299
|
+
if (existsSync(join(dir, relPath)))
|
|
300
|
+
detected.add(name);
|
|
301
|
+
};
|
|
302
|
+
// Root-level checks first (fast path)
|
|
303
|
+
markIfExists(join(".github", "workflows"), "github-actions");
|
|
304
|
+
markIfExists(".gitlab-ci.yml", "gitlab");
|
|
305
|
+
markIfExists("Jenkinsfile", "jenkins");
|
|
306
|
+
markIfExists(join(".circleci", "config.yml"), "circleci");
|
|
307
|
+
markIfExists("vercel.json", "vercel");
|
|
308
|
+
markIfExists(".vercel", "vercel");
|
|
309
|
+
markIfExists("netlify.toml", "netlify");
|
|
310
|
+
markIfExists("fly.toml", "fly");
|
|
311
|
+
markIfExists("railway.json", "railway");
|
|
312
|
+
markIfExists("railway.toml", "railway");
|
|
313
|
+
markIfExists("render.yaml", "render");
|
|
314
|
+
markIfExists("Dockerfile", "docker");
|
|
315
|
+
markIfExists("docker-compose.yml", "docker");
|
|
316
|
+
markIfExists("docker-compose.yaml", "docker");
|
|
317
|
+
markIfExists("terraform", "terraform");
|
|
318
|
+
markIfExists("main.tf", "terraform");
|
|
319
|
+
markIfExists("pulumi.yaml", "pulumi");
|
|
320
|
+
// Monorepo-aware bounded walk for deployment files in sub-app folders.
|
|
321
|
+
const MAX_DEPTH = 3;
|
|
322
|
+
const walk = (current, depth) => {
|
|
323
|
+
if (depth > MAX_DEPTH)
|
|
324
|
+
return;
|
|
325
|
+
let entries = [];
|
|
326
|
+
try {
|
|
327
|
+
entries = readdirSync(current);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
for (const entry of entries) {
|
|
333
|
+
if (entry.startsWith("."))
|
|
334
|
+
continue;
|
|
335
|
+
if (SKIP_DIRS.has(entry))
|
|
336
|
+
continue;
|
|
337
|
+
const full = join(current, entry);
|
|
338
|
+
let isDir = false;
|
|
339
|
+
try {
|
|
340
|
+
isDir = statSync(full).isDirectory();
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (!isDir)
|
|
346
|
+
continue;
|
|
347
|
+
if (existsSync(join(full, "vercel.json")) ||
|
|
348
|
+
existsSync(join(full, ".vercel")))
|
|
349
|
+
detected.add("vercel");
|
|
350
|
+
if (existsSync(join(full, "netlify.toml")))
|
|
351
|
+
detected.add("netlify");
|
|
352
|
+
if (existsSync(join(full, "fly.toml")))
|
|
353
|
+
detected.add("fly");
|
|
354
|
+
if (existsSync(join(full, "railway.json")) || existsSync(join(full, "railway.toml")))
|
|
355
|
+
detected.add("railway");
|
|
356
|
+
if (existsSync(join(full, "render.yaml")))
|
|
357
|
+
detected.add("render");
|
|
358
|
+
if (existsSync(join(full, "Dockerfile")))
|
|
359
|
+
detected.add("docker");
|
|
360
|
+
if (existsSync(join(full, "docker-compose.yml")) || existsSync(join(full, "docker-compose.yaml")))
|
|
361
|
+
detected.add("docker");
|
|
362
|
+
if (existsSync(join(full, "main.tf")))
|
|
363
|
+
detected.add("terraform");
|
|
364
|
+
if (existsSync(join(full, "pulumi.yaml")))
|
|
365
|
+
detected.add("pulumi");
|
|
366
|
+
walk(full, depth + 1);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
walk(dir, 1);
|
|
370
|
+
return Array.from(detected);
|
|
272
371
|
}
|
|
273
372
|
const SKIP_DIRS = new Set([
|
|
274
373
|
// JavaScript / Node
|
|
@@ -286,14 +385,30 @@ const SKIP_DIRS = new Set([
|
|
|
286
385
|
// Generic
|
|
287
386
|
"build", ".git", ".cache", "tmp",
|
|
288
387
|
]);
|
|
388
|
+
const ARCHITECTURAL_DEV_DEP_ALLOWLIST = new Set([
|
|
389
|
+
"tailwindcss",
|
|
390
|
+
"turbo",
|
|
391
|
+
"turborepo",
|
|
392
|
+
"@vercel/analytics",
|
|
393
|
+
"@vercel/speed-insights",
|
|
394
|
+
"@vercel/postgres",
|
|
395
|
+
"@vercel/blob",
|
|
396
|
+
"@neondatabase/serverless",
|
|
397
|
+
]);
|
|
289
398
|
function collectPackageDeps(pkgPath) {
|
|
290
399
|
try {
|
|
291
400
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
292
|
-
// devDependencies are build/test/type tooling — no architectural value
|
|
293
401
|
const merged = {
|
|
294
|
-
...pkg.dependencies,
|
|
295
|
-
...pkg.peerDependencies,
|
|
402
|
+
...(pkg.dependencies ?? {}),
|
|
403
|
+
...(pkg.peerDependencies ?? {}),
|
|
296
404
|
};
|
|
405
|
+
// Include a small set of architecturally significant dev dependencies
|
|
406
|
+
// (hosting/build/runtime-defining tools) while still filtering test-only noise.
|
|
407
|
+
for (const [name, versionRaw] of Object.entries(pkg.devDependencies ?? {})) {
|
|
408
|
+
if (ARCHITECTURAL_DEV_DEP_ALLOWLIST.has(name)) {
|
|
409
|
+
merged[name] = versionRaw;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
297
412
|
return Object.entries(merged).map(([name, versionRaw]) => ({
|
|
298
413
|
name,
|
|
299
414
|
versionRaw: typeof versionRaw === "string" ? versionRaw : null,
|
|
@@ -696,12 +811,12 @@ function scanProject(dir) {
|
|
|
696
811
|
catch { }
|
|
697
812
|
}
|
|
698
813
|
// ── .env files ─────────────────────────────────────────────────────────────
|
|
699
|
-
for (const envFile of [".env", ".env.example", ".env.local", ".env.runtime"]) {
|
|
814
|
+
for (const envFile of [".env", ".env.example", ".env.local", ".env.runtime", ".env.production", ".env.development"]) {
|
|
700
815
|
const envPath = join(dir, envFile);
|
|
701
816
|
if (existsSync(envPath)) {
|
|
702
817
|
try {
|
|
703
|
-
for (const
|
|
704
|
-
names.add(
|
|
818
|
+
for (const signal of parseEnvSignals(readFileSync(envPath, "utf8")))
|
|
819
|
+
names.add(signal);
|
|
705
820
|
}
|
|
706
821
|
catch { }
|
|
707
822
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import process from "process";
|
|
2
|
+
import { requireCredentials } from "../lib/credentials.js";
|
|
3
|
+
import { callMcpTool } from "../lib/mcp.js";
|
|
4
|
+
function hasFlag(args, flag) {
|
|
5
|
+
return args.includes(flag);
|
|
6
|
+
}
|
|
7
|
+
function option(args, key) {
|
|
8
|
+
const i = args.indexOf(key);
|
|
9
|
+
if (i === -1)
|
|
10
|
+
return null;
|
|
11
|
+
const v = args[i + 1];
|
|
12
|
+
if (!v || v.startsWith("--"))
|
|
13
|
+
return null;
|
|
14
|
+
return v;
|
|
15
|
+
}
|
|
16
|
+
export async function listEntities(args) {
|
|
17
|
+
const asJson = hasFlag(args, "--json");
|
|
18
|
+
const entityTypeCode = option(args, "--type") ?? undefined;
|
|
19
|
+
const status = option(args, "--status") ?? undefined;
|
|
20
|
+
const query = option(args, "--query") ?? undefined;
|
|
21
|
+
const limitRaw = option(args, "--limit");
|
|
22
|
+
const limit = limitRaw ? Number(limitRaw) : undefined;
|
|
23
|
+
if (limitRaw && (!Number.isInteger(limit) || (limit ?? 0) < 1 || (limit ?? 0) > 500)) {
|
|
24
|
+
console.error("error: --limit must be an integer between 1 and 500");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const creds = requireCredentials();
|
|
28
|
+
const raw = await callMcpTool("nexarch_list_entities", {
|
|
29
|
+
companyId: creds.companyId,
|
|
30
|
+
entityTypeCode,
|
|
31
|
+
status,
|
|
32
|
+
query,
|
|
33
|
+
limit,
|
|
34
|
+
}, { companyId: creds.companyId });
|
|
35
|
+
const result = JSON.parse(raw.content?.[0]?.text ?? "{}");
|
|
36
|
+
if (asJson) {
|
|
37
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.log(`Found ${result.count ?? 0} entities`);
|
|
41
|
+
for (const e of result.entities ?? []) {
|
|
42
|
+
const subtype = e.entitySubtypeCode ? `/${e.entitySubtypeCode}` : "";
|
|
43
|
+
const key = e.externalKey ? ` (${e.externalKey})` : "";
|
|
44
|
+
console.log(`- ${e.name}${key} [${e.entityTypeCode}${subtype}] status=${e.status}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import process from "process";
|
|
2
|
+
import { requireCredentials } from "../lib/credentials.js";
|
|
3
|
+
import { callMcpTool } from "../lib/mcp.js";
|
|
4
|
+
function hasFlag(args, flag) {
|
|
5
|
+
return args.includes(flag);
|
|
6
|
+
}
|
|
7
|
+
function option(args, key) {
|
|
8
|
+
const i = args.indexOf(key);
|
|
9
|
+
if (i === -1)
|
|
10
|
+
return null;
|
|
11
|
+
const v = args[i + 1];
|
|
12
|
+
if (!v || v.startsWith("--"))
|
|
13
|
+
return null;
|
|
14
|
+
return v;
|
|
15
|
+
}
|
|
16
|
+
export async function listRelationships(args) {
|
|
17
|
+
const asJson = hasFlag(args, "--json");
|
|
18
|
+
const relationshipTypeCode = option(args, "--type") ?? undefined;
|
|
19
|
+
const status = option(args, "--status") ?? undefined;
|
|
20
|
+
const fromEntityExternalKey = option(args, "--from") ?? undefined;
|
|
21
|
+
const toEntityExternalKey = option(args, "--to") ?? undefined;
|
|
22
|
+
const limitRaw = option(args, "--limit");
|
|
23
|
+
const limit = limitRaw ? Number(limitRaw) : undefined;
|
|
24
|
+
if (limitRaw && (!Number.isInteger(limit) || (limit ?? 0) < 1 || (limit ?? 0) > 500)) {
|
|
25
|
+
console.error("error: --limit must be an integer between 1 and 500");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const creds = requireCredentials();
|
|
29
|
+
const raw = await callMcpTool("nexarch_list_relationships", {
|
|
30
|
+
companyId: creds.companyId,
|
|
31
|
+
relationshipTypeCode,
|
|
32
|
+
status,
|
|
33
|
+
fromEntityExternalKey,
|
|
34
|
+
toEntityExternalKey,
|
|
35
|
+
limit,
|
|
36
|
+
}, { companyId: creds.companyId });
|
|
37
|
+
const result = JSON.parse(raw.content?.[0]?.text ?? "{}");
|
|
38
|
+
if (asJson) {
|
|
39
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log(`Found ${result.count ?? 0} relationships`);
|
|
43
|
+
for (const r of result.relationships ?? []) {
|
|
44
|
+
const subtype = r.relationshipSubtypeCode ? `/${r.relationshipSubtypeCode}` : "";
|
|
45
|
+
const from = r.fromEntityExternalKey ?? r.fromEntityName ?? "?";
|
|
46
|
+
const to = r.toEntityExternalKey ?? r.toEntityName ?? "?";
|
|
47
|
+
console.log(`- ${from} -[${r.relationshipTypeCode}${subtype}|${r.status}]-> ${to}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -83,18 +83,23 @@ function parseFindings(args) {
|
|
|
83
83
|
}
|
|
84
84
|
return parsed.map((item) => {
|
|
85
85
|
const value = item;
|
|
86
|
+
const policyControlId = String(value.policyControlId ?? value.controlId ?? "").trim();
|
|
87
|
+
const policyRuleId = String(value.policyRuleId ?? value.ruleId ?? "").trim();
|
|
86
88
|
const result = String(value.result ?? "").toLowerCase();
|
|
87
|
-
if (!
|
|
88
|
-
throw new Error("Each finding must include policyControlId, policyRuleId, result");
|
|
89
|
+
if (!policyControlId || !policyRuleId || !result) {
|
|
90
|
+
throw new Error("Each finding must include policyControlId, policyRuleId, result. Tip: run 'nexarch policy-controls --entity <application:key> --json' and use the rule IDs from controls[].rules[].id");
|
|
89
91
|
}
|
|
90
92
|
if (result !== "pass" && result !== "partial" && result !== "fail") {
|
|
91
93
|
throw new Error(`Invalid finding result '${String(value.result)}'. Use pass|partial|fail.`);
|
|
92
94
|
}
|
|
95
|
+
const rationale = value.rationale
|
|
96
|
+
? String(value.rationale)
|
|
97
|
+
: [value.summary, value.evidence].filter(Boolean).map(String).join("\n\n");
|
|
93
98
|
return {
|
|
94
|
-
policyControlId
|
|
95
|
-
policyRuleId
|
|
99
|
+
policyControlId,
|
|
100
|
+
policyRuleId,
|
|
96
101
|
result,
|
|
97
|
-
...(
|
|
102
|
+
...(rationale ? { rationale } : {}),
|
|
98
103
|
...(Array.isArray(value.missingRequirements) ? { missingRequirements: value.missingRequirements.map(String) } : {}),
|
|
99
104
|
};
|
|
100
105
|
});
|
|
@@ -107,6 +112,27 @@ function parseFindings(args) {
|
|
|
107
112
|
}
|
|
108
113
|
export async function policyAuditSubmit(args) {
|
|
109
114
|
const asJson = parseFlag(args, "--json");
|
|
115
|
+
if (parseFlag(args, "--help") || parseFlag(args, "-h")) {
|
|
116
|
+
console.log(`
|
|
117
|
+
Usage:
|
|
118
|
+
nexarch policy-audit-submit --command-id <id> --application-key <key> [options]
|
|
119
|
+
|
|
120
|
+
Options:
|
|
121
|
+
--command-id <id> Required command id
|
|
122
|
+
--application-key <key> Required application key (e.g. application:bad-driving)
|
|
123
|
+
--agent-key <key> Optional agent key (defaults from identity)
|
|
124
|
+
--finding <controlId|ruleId|result|rationale|missing1;missing2> Repeatable
|
|
125
|
+
--findings-json <json> JSON array of findings
|
|
126
|
+
--findings-file <path> Path to JSON array of findings
|
|
127
|
+
--json Print JSON response
|
|
128
|
+
|
|
129
|
+
Notes:
|
|
130
|
+
- Findings are rule-level (policyRuleId is required).
|
|
131
|
+
- You can submit partial findings multiple times for the same command.
|
|
132
|
+
- Get valid rule ids with: nexarch policy-controls --entity <application:key> --json
|
|
133
|
+
`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
110
136
|
const commandId = parseOptionValue(args, "--command-id") ?? parseOptionValue(args, "--id");
|
|
111
137
|
const applicationEntityKey = parseOptionValue(args, "--application-key") ?? parseOptionValue(args, "--entity");
|
|
112
138
|
const agentKey = parseOptionValue(args, "--agent-key") ?? loadIdentityAgentKey();
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import process from "process";
|
|
2
|
+
import { writeFileSync } from "fs";
|
|
3
|
+
import { requireCredentials } from "../lib/credentials.js";
|
|
4
|
+
import { callMcpTool } from "../lib/mcp.js";
|
|
5
|
+
function parseFlag(args, flag) {
|
|
6
|
+
return args.includes(flag);
|
|
7
|
+
}
|
|
8
|
+
function parseOptionValue(args, option) {
|
|
9
|
+
const idx = args.indexOf(option);
|
|
10
|
+
if (idx === -1)
|
|
11
|
+
return null;
|
|
12
|
+
const v = args[idx + 1];
|
|
13
|
+
if (!v || v.startsWith("--"))
|
|
14
|
+
return null;
|
|
15
|
+
return v;
|
|
16
|
+
}
|
|
17
|
+
function parseMultiOptionValues(args, option) {
|
|
18
|
+
const values = [];
|
|
19
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
20
|
+
if (args[i] !== option)
|
|
21
|
+
continue;
|
|
22
|
+
const v = args[i + 1];
|
|
23
|
+
if (!v || v.startsWith("--"))
|
|
24
|
+
continue;
|
|
25
|
+
values.push(v);
|
|
26
|
+
}
|
|
27
|
+
return values;
|
|
28
|
+
}
|
|
29
|
+
function parseToolText(result) {
|
|
30
|
+
const text = result.content?.[0]?.text ?? "{}";
|
|
31
|
+
return JSON.parse(text);
|
|
32
|
+
}
|
|
33
|
+
export async function policyAuditTemplate(args) {
|
|
34
|
+
const asJson = parseFlag(args, "--json");
|
|
35
|
+
const entity = parseOptionValue(args, "--entity") ?? parseOptionValue(args, "--application-key");
|
|
36
|
+
const outputPath = parseOptionValue(args, "--out") ?? parseOptionValue(args, "--output");
|
|
37
|
+
const defaultResult = (parseOptionValue(args, "--default-result") ?? "fail").toLowerCase();
|
|
38
|
+
const selectedControlIds = new Set(parseMultiOptionValues(args, "--control-id"));
|
|
39
|
+
if (parseFlag(args, "--help") || parseFlag(args, "-h")) {
|
|
40
|
+
console.log(`
|
|
41
|
+
Usage:
|
|
42
|
+
nexarch policy-audit-template --entity <application:key> [options]
|
|
43
|
+
|
|
44
|
+
Options:
|
|
45
|
+
--entity <key> Required (e.g. application:bad-driving)
|
|
46
|
+
--control-id <uuid> Optional repeatable filter (selected controls only)
|
|
47
|
+
--default-result <value> pass|partial|fail (default: fail)
|
|
48
|
+
--output, --out <path.json> Write findings array to file
|
|
49
|
+
--json Print JSON to stdout
|
|
50
|
+
|
|
51
|
+
Output shape is ready for:
|
|
52
|
+
nexarch policy-audit-submit --findings-file <path.json>
|
|
53
|
+
`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!entity) {
|
|
57
|
+
console.error("error: --entity <externalKey> is required (e.g. application:bad-driving)");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
if (!["pass", "partial", "fail"].includes(defaultResult)) {
|
|
61
|
+
console.error("error: --default-result must be pass|partial|fail");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const creds = requireCredentials();
|
|
65
|
+
const raw = await callMcpTool("nexarch_get_entity_policy_controls", { entityExternalKey: entity, companyId: creds.companyId }, { companyId: creds.companyId });
|
|
66
|
+
const result = parseToolText(raw);
|
|
67
|
+
if (!result.found) {
|
|
68
|
+
console.error(`error: entity not found: ${entity}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const findings = (result.controls ?? [])
|
|
72
|
+
.filter((control) => selectedControlIds.size === 0 || selectedControlIds.has(control.id))
|
|
73
|
+
.flatMap((control) => (control.rules ?? []).map((rule) => ({
|
|
74
|
+
policyControlId: control.id,
|
|
75
|
+
policyRuleId: rule.id,
|
|
76
|
+
result: defaultResult,
|
|
77
|
+
rationale: "",
|
|
78
|
+
missingRequirements: [],
|
|
79
|
+
})));
|
|
80
|
+
if (findings.length === 0) {
|
|
81
|
+
console.error("error: no rules found for selected controls/entity");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
const payload = JSON.stringify(findings, null, 2);
|
|
85
|
+
if (outputPath) {
|
|
86
|
+
writeFileSync(outputPath, `${payload}\n`, "utf8");
|
|
87
|
+
if (!asJson) {
|
|
88
|
+
console.log(`Template written: ${outputPath}`);
|
|
89
|
+
console.log(`Findings: ${findings.length}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (asJson || !outputPath) {
|
|
93
|
+
process.stdout.write(`${payload}\n`);
|
|
94
|
+
}
|
|
95
|
+
}
|
package/dist/commands/status.js
CHANGED
|
@@ -31,7 +31,17 @@ export async function status(_args) {
|
|
|
31
31
|
if (registryInfo) {
|
|
32
32
|
console.log(`\n Integration registry: v${registryInfo.version} (published ${new Date(registryInfo.publishedAt).toLocaleString()})`);
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
const architectureFacts = governance.architectureFactCount ?? governance.canonicalFactCount;
|
|
35
|
+
console.log(`\n Architecture facts: ${architectureFacts}`);
|
|
36
|
+
if (typeof governance.graphEntityCount === "number" && typeof governance.graphRelationshipCount === "number") {
|
|
37
|
+
console.log(` Graph breakdown: ${governance.graphEntityCount} entities, ${governance.graphRelationshipCount} relationships`);
|
|
38
|
+
}
|
|
39
|
+
if (typeof governance.canonicalCoverageOverallPct === "number") {
|
|
40
|
+
console.log(` Canonical coverage: ${governance.canonicalCoverageOverallPct}%`);
|
|
41
|
+
}
|
|
42
|
+
if (typeof governance.canonicalRegistryCount === "number") {
|
|
43
|
+
console.log(` Canonical registry: ${governance.canonicalRegistryCount}`);
|
|
44
|
+
}
|
|
35
45
|
console.log(` Pending review: ${governance.reviewQueue.pending_entities} entities, ${governance.reviewQueue.pending_relationships} relationships`);
|
|
36
46
|
if (governance.latestSnapshot) {
|
|
37
47
|
const snapshotDate = new Date(governance.latestSnapshot.created_at).toLocaleDateString();
|
package/dist/index.js
CHANGED
|
@@ -12,11 +12,14 @@ import { updateEntity } from "./commands/update-entity.js";
|
|
|
12
12
|
import { addRelationship } from "./commands/add-relationship.js";
|
|
13
13
|
import { registerAlias } from "./commands/register-alias.js";
|
|
14
14
|
import { resolveNames } from "./commands/resolve-names.js";
|
|
15
|
+
import { listEntities } from "./commands/list-entities.js";
|
|
16
|
+
import { listRelationships } from "./commands/list-relationships.js";
|
|
15
17
|
import { checkIn } from "./commands/check-in.js";
|
|
16
18
|
import { commandDone } from "./commands/command-done.js";
|
|
17
19
|
import { commandFail } from "./commands/command-fail.js";
|
|
18
20
|
import { commandClaim } from "./commands/command-claim.js";
|
|
19
21
|
import { policyControls } from "./commands/policy-controls.js";
|
|
22
|
+
import { policyAuditTemplate } from "./commands/policy-audit-template.js";
|
|
20
23
|
import { policyAuditSubmit } from "./commands/policy-audit-submit.js";
|
|
21
24
|
const [, , command, ...args] = process.argv;
|
|
22
25
|
const commands = {
|
|
@@ -33,11 +36,14 @@ const commands = {
|
|
|
33
36
|
"add-relationship": addRelationship,
|
|
34
37
|
"register-alias": registerAlias,
|
|
35
38
|
"resolve-names": resolveNames,
|
|
39
|
+
"list-entities": listEntities,
|
|
40
|
+
"list-relationships": listRelationships,
|
|
36
41
|
"check-in": checkIn,
|
|
37
42
|
"command-done": commandDone,
|
|
38
43
|
"command-fail": commandFail,
|
|
39
44
|
"command-claim": commandClaim,
|
|
40
45
|
"policy-controls": policyControls,
|
|
46
|
+
"policy-audit-template": policyAuditTemplate,
|
|
41
47
|
"policy-audit-submit": policyAuditSubmit,
|
|
42
48
|
};
|
|
43
49
|
async function main() {
|
|
@@ -119,6 +125,21 @@ Usage:
|
|
|
119
125
|
results before calling add-relationship.
|
|
120
126
|
Options: --names <csv> (required, e.g. "vercel,neon")
|
|
121
127
|
--json
|
|
128
|
+
nexarch list-entities
|
|
129
|
+
List entities from the workspace graph.
|
|
130
|
+
Options: --type <entityTypeCode>
|
|
131
|
+
--status <status>
|
|
132
|
+
--query <text>
|
|
133
|
+
--limit <1-500>
|
|
134
|
+
--json
|
|
135
|
+
nexarch list-relationships
|
|
136
|
+
List relationships from the workspace graph.
|
|
137
|
+
Options: --type <relationshipTypeCode>
|
|
138
|
+
--status <status>
|
|
139
|
+
--from <fromExternalKey>
|
|
140
|
+
--to <toExternalKey>
|
|
141
|
+
--limit <1-500>
|
|
142
|
+
--json
|
|
122
143
|
nexarch check-in Preview pending application-target commands (no auto-claim).
|
|
123
144
|
Scope is resolved server-side from active company context.
|
|
124
145
|
Options: --agent-key <key> override stored agent key
|
|
@@ -143,6 +164,13 @@ Usage:
|
|
|
143
164
|
Fetch policy controls/rules assigned to an entity (for policy audits).
|
|
144
165
|
Options: --entity <externalKey> (required, e.g. application:bad-driving)
|
|
145
166
|
--json
|
|
167
|
+
nexarch policy-audit-template
|
|
168
|
+
Generate a findings JSON template from policy controls/rules for an entity.
|
|
169
|
+
Options: --entity <externalKey> (required)
|
|
170
|
+
--control-id <uuid> (repeatable; optional filter)
|
|
171
|
+
--default-result <pass|partial|fail> (default: fail)
|
|
172
|
+
--output <path.json>
|
|
173
|
+
--json
|
|
146
174
|
nexarch policy-audit-submit
|
|
147
175
|
Submit structured policy findings (writes policy_audit_finding rows).
|
|
148
176
|
Options: --command-id <id> (required)
|