filegrc 0.3.0 → 0.3.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/README.md +3 -3
- package/package.json +1 -1
- package/src/cli.js +51 -12
- package/src/index.js +1 -1
- package/src/program-path.js +1 -1
- package/src/program-readiness.js +33 -2
- package/src/setup.js +104 -66
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ npx filegrc list risk --json
|
|
|
26
26
|
npx filegrc references risk-example --json
|
|
27
27
|
npx filegrc describe risk
|
|
28
28
|
npx filegrc search "access review"
|
|
29
|
-
npx filegrc evidence-test-drafts
|
|
29
|
+
npx filegrc evidence-test-drafts --preview --json
|
|
30
30
|
npx filegrc program-readiness --summary --json
|
|
31
31
|
npx filegrc program-readiness --require-ready
|
|
32
32
|
npx filegrc audit-readiness audit-id
|
|
@@ -36,13 +36,13 @@ npx filegrc evidence-packet --start 2026-01-01 --end 2026-06-30 --audit audit-id
|
|
|
36
36
|
|
|
37
37
|
`filegrc serve --help` prints bind, port, environment, and safety options without starting the server. The editable server defaults to `127.0.0.1:8787`; set `FILEGRC_HOST`, `FILEGRC_PORT`, or the matching flags when needed.
|
|
38
38
|
|
|
39
|
-
`filegrc setup` provides the headless equivalent of browser onboarding. Run it without arguments for guided terminal setup, or pass all initial service-boundary fields and a management program goal as flags or a JSON payload. Selecting Type 1 or Type 2 updates the workspace goal and
|
|
39
|
+
`filegrc setup` provides the headless equivalent of browser onboarding. Run it without arguments for guided terminal setup, or pass all initial service-boundary fields and a management program goal as flags or a JSON payload. Add `--preview` to validate and inspect the planned service and workspace writes without saving. Add `--summary --json` for compact agent output. Selecting Type 1 or Type 2 updates the workspace goal and selected systems. Setup does not select framework records, link controls, create evidence, or create an audit record.
|
|
40
40
|
|
|
41
41
|
`filegrc program-path` gives agents the renderer’s six-step order, exact page Instructions, Use, Policy Basis, commands, current state, and next actions. `filegrc guide <type>` repeats the matching page guidance and adds fields, relationship candidates, Markdown slots, and timing for that record type.
|
|
42
42
|
|
|
43
43
|
`filegrc program-readiness` reports whether management can start a candidate Type 2 period. Add `--summary --json` for compact stage counts and next actions, or omit `--summary` for every readiness item. Use `--require-ready` in automation. The command does not require an audit ID or CPA firm.
|
|
44
44
|
|
|
45
|
-
`filegrc evidence-test-drafts`
|
|
45
|
+
`filegrc evidence-test-drafts --preview --json` reports the missing draft tests for external evidence without writing them. Run `filegrc evidence-test-drafts` after confirming applicable controls and authoritative source Systems. When a Step 5 operating record exists, put the fixed artifact in an External Evidence record and link it from that operating record.
|
|
46
46
|
|
|
47
47
|
`filegrc obligations` shows recurring work and a task-level preview for each Policy Event, including owners, deadlines, and requested proof. `filegrc trigger` adds the event and all of its Action Items to the Work Queue atomically, then prints the created task IDs and deadlines.
|
|
48
48
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldRes
|
|
|
6
6
|
import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
|
|
7
7
|
import { buildWorkspace } from "./build.js";
|
|
8
8
|
import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
|
|
9
|
-
import { ensureEvidenceTestDrafts } from "./evidence-tests.js";
|
|
9
|
+
import { ensureEvidenceTestDrafts, planEvidenceTestDrafts } from "./evidence-tests.js";
|
|
10
10
|
import {
|
|
11
11
|
addEvidenceAttachment,
|
|
12
12
|
createResource,
|
|
@@ -28,7 +28,7 @@ import { assessProgramReadiness } from "./program-readiness.js";
|
|
|
28
28
|
import { markdownEntries } from "./resource-markdown.js";
|
|
29
29
|
import { searchResources } from "./search.js";
|
|
30
30
|
import { serveWorkspace } from "./server.js";
|
|
31
|
-
import { setupWorkspace } from "./setup.js";
|
|
31
|
+
import { planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
|
|
32
32
|
import { createAppState } from "./state.js";
|
|
33
33
|
import { currentCalendarDate } from "./time.js";
|
|
34
34
|
import { validateWorkspace } from "./validate.js";
|
|
@@ -83,15 +83,22 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
83
83
|
...(flags["program-goal"] !== undefined ? { programGoal: flags["program-goal"] } : {}),
|
|
84
84
|
...(flags.draft ? { draft: true } : {})
|
|
85
85
|
});
|
|
86
|
-
const result =
|
|
87
|
-
|
|
86
|
+
const result = flags.preview
|
|
87
|
+
? await planWorkspaceSetup(root, setupInput)
|
|
88
|
+
: await setupWorkspace(root, setupInput);
|
|
89
|
+
const output = flags.summary && !flags.preview ? summarizeSetupResult(result) : result;
|
|
90
|
+
if (flags.json) console.log(JSON.stringify(output, null, 2));
|
|
91
|
+
else if (flags.preview) {
|
|
92
|
+
console.log(`Setup preview: ${result.changes.system} system ${result.system.id}; update workspace target to ${result.target.assuranceGoal}.`);
|
|
93
|
+
console.log("No controls will be linked and no evidence drafts will be created.");
|
|
94
|
+
}
|
|
88
95
|
else {
|
|
89
96
|
console.log(`${result.draft ? "Saved draft scope" : "Completed initial setup"} for ${result.system.title}.`);
|
|
90
97
|
console.log(`System: ${result.system.id} (${result.system.status})`);
|
|
91
98
|
console.log(`Target: ${result.workspace.assuranceGoal}`);
|
|
92
99
|
console.log("Next: finish Step 1 by confirming people, criteria, commitments, vendors, and in-scope systems. Run filegrc program-path for the full path.");
|
|
93
100
|
}
|
|
94
|
-
return
|
|
101
|
+
return output;
|
|
95
102
|
}
|
|
96
103
|
if (command === "build") {
|
|
97
104
|
const result = await buildWorkspace(positionals[0] ?? root, { output: flags.output });
|
|
@@ -253,6 +260,29 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
253
260
|
return output;
|
|
254
261
|
}
|
|
255
262
|
if (command === "evidence-test-drafts") {
|
|
263
|
+
if (flags.preview) {
|
|
264
|
+
const loaded = await loadWorkspace(root);
|
|
265
|
+
const plan = planEvidenceTestDrafts(loaded);
|
|
266
|
+
const result = {
|
|
267
|
+
schemaVersion: 1,
|
|
268
|
+
preview: true,
|
|
269
|
+
total: plan.length,
|
|
270
|
+
create: plan.filter(({ existing }) => !existing).map((item) => ({
|
|
271
|
+
familyId: item.familyId,
|
|
272
|
+
title: item.title,
|
|
273
|
+
testEvidenceKind: item.testEvidenceKind,
|
|
274
|
+
controlIds: item.controlIds
|
|
275
|
+
})),
|
|
276
|
+
existing: plan.filter(({ existing }) => existing).map(({ existing }) => ({
|
|
277
|
+
id: existing.id,
|
|
278
|
+
title: existing.title,
|
|
279
|
+
status: existing.status
|
|
280
|
+
}))
|
|
281
|
+
};
|
|
282
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
283
|
+
else console.log(`Evidence draft preview: create ${result.create.length}; preserve ${result.existing.length}.`);
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
256
286
|
const result = await ensureEvidenceTestDrafts(root);
|
|
257
287
|
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
258
288
|
else console.log(`Created ${result.created.length} External Evidence test ${result.created.length === 1 ? "draft" : "drafts"}; ${result.total} required families are represented.`);
|
|
@@ -641,7 +671,7 @@ function printHelp() {
|
|
|
641
671
|
|
|
642
672
|
Usage:
|
|
643
673
|
filegrc serve [root] [--host 127.0.0.1] [--port 8787]
|
|
644
|
-
filegrc setup [setup.json|-] [setup options] [--draft] [--json]
|
|
674
|
+
filegrc setup [setup.json|-] [setup options] [--draft] [--preview] [--summary] [--json]
|
|
645
675
|
filegrc build [root] [--output .filegrc/site]
|
|
646
676
|
filegrc validate [root] [--json]
|
|
647
677
|
filegrc model [--json|--write-docs|--check-docs]
|
|
@@ -654,7 +684,7 @@ Usage:
|
|
|
654
684
|
filegrc search <query> [--type resource-type] [--json]
|
|
655
685
|
filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
|
|
656
686
|
filegrc program-readiness [--as-of YYYY-MM-DD] [--require-ready] [--summary] [--json]
|
|
657
|
-
filegrc evidence-test-drafts [--json]
|
|
687
|
+
filegrc evidence-test-drafts [--preview] [--json]
|
|
658
688
|
filegrc audit-readiness [audit-id] [--require-ready] [--json]
|
|
659
689
|
filegrc prepare-audit <audit-id> [--json]
|
|
660
690
|
filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--subject resource-id[,resource-id]] [--title text] [--json]
|
|
@@ -707,6 +737,8 @@ Options:
|
|
|
707
737
|
--internet-exposed <bool> true or false
|
|
708
738
|
--program-goal <goal> none, readiness, type-1, or type-2
|
|
709
739
|
--draft Save the service boundary as planned
|
|
740
|
+
--preview Validate and report planned writes without saving
|
|
741
|
+
--summary Omit full workspace relationship arrays
|
|
710
742
|
--json Print the result as JSON
|
|
711
743
|
--root <path> Workspace path
|
|
712
744
|
--help Show this help`);
|
|
@@ -750,10 +782,12 @@ Options:
|
|
|
750
782
|
console.log(`Usage:
|
|
751
783
|
filegrc evidence-test-drafts [options]
|
|
752
784
|
|
|
753
|
-
|
|
754
|
-
already have a dedicated Step 5 operating record. Existing tests are
|
|
785
|
+
Preview or create missing draft External Evidence records for collection that
|
|
786
|
+
does not already have a dedicated Step 5 operating record. Existing tests are
|
|
787
|
+
preserved. Run after confirming applicable controls and source systems.
|
|
755
788
|
|
|
756
789
|
Options:
|
|
790
|
+
--preview Report proposed drafts without creating them
|
|
757
791
|
--json Print created and existing records as JSON
|
|
758
792
|
--root <path> Workspace path
|
|
759
793
|
--help Show this help`);
|
|
@@ -770,7 +804,7 @@ function agentOverview(model) {
|
|
|
770
804
|
help: "filegrc help",
|
|
771
805
|
version: "filegrc version",
|
|
772
806
|
serve: "filegrc serve [root]",
|
|
773
|
-
setup: "filegrc setup [setup.json|-] [--draft] [--json]",
|
|
807
|
+
setup: "filegrc setup [setup.json|-] [--draft] [--preview] [--summary] [--json]",
|
|
774
808
|
build: "filegrc build [root]",
|
|
775
809
|
validate: "filegrc validate [root] --json",
|
|
776
810
|
model: "filegrc model --json",
|
|
@@ -783,7 +817,7 @@ function agentOverview(model) {
|
|
|
783
817
|
search: "filegrc search <query> --json",
|
|
784
818
|
obligations: "filegrc obligations --json",
|
|
785
819
|
programReadiness: "filegrc program-readiness --json",
|
|
786
|
-
evidenceTestDrafts: "filegrc evidence-test-drafts --json",
|
|
820
|
+
evidenceTestDrafts: "filegrc evidence-test-drafts --preview --json",
|
|
787
821
|
auditReadiness: "filegrc audit-readiness <audit-id> --json",
|
|
788
822
|
prepareAudit: "filegrc prepare-audit <audit-id>",
|
|
789
823
|
trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
|
|
@@ -924,13 +958,17 @@ function printProgramPath(result) {
|
|
|
924
958
|
}
|
|
925
959
|
|
|
926
960
|
function summarizeProgramReadiness(result) {
|
|
961
|
+
const ownership = result.stages
|
|
962
|
+
.flatMap((stage) => stage.items)
|
|
963
|
+
.find((item) => item.id === "program-ownership");
|
|
927
964
|
const summarizeItem = (item) => item ? {
|
|
928
965
|
id: item.id,
|
|
929
966
|
status: item.status,
|
|
930
967
|
title: item.title,
|
|
931
968
|
message: item.message,
|
|
932
969
|
...(item.resourceType ? { resourceType: item.resourceType } : {}),
|
|
933
|
-
...(item.resourceId ? { resourceId: item.resourceId } : {})
|
|
970
|
+
...(item.resourceId ? { resourceId: item.resourceId } : {}),
|
|
971
|
+
...(item.unresolvedAssignments?.length ? { unresolvedAssignments: item.unresolvedAssignments } : {})
|
|
934
972
|
} : null;
|
|
935
973
|
return {
|
|
936
974
|
schemaVersion: result.schemaVersion,
|
|
@@ -947,6 +985,7 @@ function summarizeProgramReadiness(result) {
|
|
|
947
985
|
scopeCounts: Object.fromEntries(
|
|
948
986
|
Object.entries(result.scope).map(([name, ids]) => [name.replace(/Ids$/, ""), ids.length])
|
|
949
987
|
),
|
|
988
|
+
unresolvedOwnership: ownership?.unresolvedAssignments || [],
|
|
950
989
|
firstAction: summarizeItem(result.firstAction),
|
|
951
990
|
stages: result.stages.map((stage) => ({
|
|
952
991
|
id: stage.id,
|
package/src/index.js
CHANGED
|
@@ -51,7 +51,7 @@ export {
|
|
|
51
51
|
} from "./recurrence.js";
|
|
52
52
|
export { searchResources, searchableValues } from "./search.js";
|
|
53
53
|
export { createFilegrcServer, serveWorkspace } from "./server.js";
|
|
54
|
-
export { normalizeSetupPayload, setupWorkspace } from "./setup.js";
|
|
54
|
+
export { normalizeSetupPayload, planWorkspaceSetup, setupWorkspace, summarizeSetupResult } from "./setup.js";
|
|
55
55
|
export { createAppState } from "./state.js";
|
|
56
56
|
export { currentCalendarDate, formatCalendarDate, formatLocalDateTime } from "./time.js";
|
|
57
57
|
export { validateWorkspace } from "./validate.js";
|
package/src/program-path.js
CHANGED
|
@@ -119,7 +119,7 @@ export const PROGRAM_PATH = [
|
|
|
119
119
|
],
|
|
120
120
|
resourceTypes: ["evidence"],
|
|
121
121
|
commands: [
|
|
122
|
-
"filegrc evidence-test-drafts --json",
|
|
122
|
+
"filegrc evidence-test-drafts --preview --json",
|
|
123
123
|
"filegrc guide evidence --json",
|
|
124
124
|
"filegrc list evidence --json",
|
|
125
125
|
"filegrc program-readiness --json"
|
package/src/program-readiness.js
CHANGED
|
@@ -203,9 +203,21 @@ function programOwnershipItem(records, byId) {
|
|
|
203
203
|
&& ![...oversightChairs].some((id) => policyOwnerIds.has(id))
|
|
204
204
|
);
|
|
205
205
|
const complete = currentOwners.size > 0 && unresolved.length === 0 && oversightComplete;
|
|
206
|
+
const unresolvedAssignments = unresolved.map((record) => ({
|
|
207
|
+
resourceType: record.type,
|
|
208
|
+
resourceId: record.id,
|
|
209
|
+
title: record.title,
|
|
210
|
+
ownerIds: record.ownerIds || [],
|
|
211
|
+
reasons: ownershipResolutionReasons(record.ownerIds || [], byId)
|
|
212
|
+
}));
|
|
206
213
|
const detail = [];
|
|
207
214
|
if (!currentOwners.size) detail.push("No current person owns the program records.");
|
|
208
|
-
if (unresolved.length)
|
|
215
|
+
if (unresolved.length) {
|
|
216
|
+
detail.push(
|
|
217
|
+
`${unresolved.length} ${unresolved.length === 1 ? "record has" : "records have"} no current person owner: ` +
|
|
218
|
+
`${unresolvedAssignments.map(({ title, resourceId }) => `${title} (${resourceId})`).join(", ")}.`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
209
221
|
if (!oversightComplete) detail.push("Finish and activate Security and Risk Oversight with a current chair who is separate from policy ownership.");
|
|
210
222
|
return item(
|
|
211
223
|
"program-ownership",
|
|
@@ -214,10 +226,29 @@ function programOwnershipItem(records, byId) {
|
|
|
214
226
|
complete
|
|
215
227
|
? `${currentOwners.size} current ${currentOwners.size === 1 ? "person owns" : "people own"} the program records.${oversight ? " Security and Risk Oversight has a separate current chair." : ""}`
|
|
216
228
|
: detail.join(" "),
|
|
217
|
-
!oversightComplete ? oversight : unresolved[0] || { type: "person" }
|
|
229
|
+
!oversightComplete ? oversight : unresolved[0] || { type: "person" },
|
|
230
|
+
{ unresolvedAssignments }
|
|
218
231
|
);
|
|
219
232
|
}
|
|
220
233
|
|
|
234
|
+
function ownershipResolutionReasons(ownerIds, byId) {
|
|
235
|
+
if (!ownerIds.length) return [{ ownerId: null, reason: "missing-owner" }];
|
|
236
|
+
return ownerIds.flatMap((ownerId) => {
|
|
237
|
+
const owner = byId.get(ownerId);
|
|
238
|
+
if (!owner) return [{ ownerId, reason: "missing-record" }];
|
|
239
|
+
if (owner.type === "person" && owner.status !== "active") {
|
|
240
|
+
return [{ ownerId, reason: "inactive-person" }];
|
|
241
|
+
}
|
|
242
|
+
if (owner.type === "team" && owner.status !== "active") {
|
|
243
|
+
return [{ ownerId, reason: "inactive-team" }];
|
|
244
|
+
}
|
|
245
|
+
if (currentPartyPeople([ownerId], byId).size === 0) {
|
|
246
|
+
return [{ ownerId, reason: owner.type === "team" ? "team-has-no-current-members" : "no-current-person" }];
|
|
247
|
+
}
|
|
248
|
+
return [];
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
221
252
|
async function policiesStage(scope, byId, readMarkdown, asOf) {
|
|
222
253
|
const linkedPolicyIds = new Set(scope.controls.flatMap((control) => control.policyIds || []));
|
|
223
254
|
const policies = [...linkedPolicyIds].map((id) => byId.get(id)).filter((record) => (
|
package/src/setup.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createResource, updateResource } from "./files.js";
|
|
2
|
-
import { ensureEvidenceTestDrafts } from "./evidence-tests.js";
|
|
3
2
|
import { createResourceId } from "./id.js";
|
|
4
3
|
import { loadWorkspace } from "./workspace.js";
|
|
5
4
|
|
|
@@ -10,81 +9,61 @@ export async function setupWorkspace(input = process.cwd(), payload = {}) {
|
|
|
10
9
|
const loaded = await loadWorkspace(input);
|
|
11
10
|
const setup = normalizeSetupPayload(payload);
|
|
12
11
|
validateSetup(loaded, setup);
|
|
12
|
+
const plan = buildSetupRecords(loaded, setup);
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
system = {
|
|
18
|
-
...(system || {}),
|
|
19
|
-
schemaVersion: 1,
|
|
20
|
-
id: systemId,
|
|
21
|
-
type: "system",
|
|
22
|
-
title: setup.serviceName,
|
|
23
|
-
status: setup.draft ? system?.status || "planned" : (system?.status === "planned" ? "active" : system?.status || "active"),
|
|
24
|
-
criticality: setup.criticality,
|
|
25
|
-
ownerIds: [setup.ownerId],
|
|
26
|
-
description: setup.boundary,
|
|
27
|
-
systemKind: system?.systemKind || "service",
|
|
28
|
-
dataClassification: setup.dataClassification,
|
|
29
|
-
internetExposed: setup.internetExposed,
|
|
30
|
-
inScope: true
|
|
31
|
-
};
|
|
32
|
-
await upsertResource(current.root, current.resources.find(({ id }) => id === systemId), system);
|
|
33
|
-
|
|
34
|
-
current = await loadWorkspace(current.root);
|
|
35
|
-
const linkedControlIds = [];
|
|
36
|
-
for (const control of current.resources.filter(({ type, status }) => (
|
|
37
|
-
type === "control" && !["not-applicable", "retired"].includes(status)
|
|
38
|
-
))) {
|
|
39
|
-
if ((control.systemIds || []).includes(systemId)) continue;
|
|
40
|
-
await updateResource(current.root, "control", control.id, {
|
|
41
|
-
...control,
|
|
42
|
-
systemIds: [...new Set([...(control.systemIds || []), systemId])]
|
|
43
|
-
});
|
|
44
|
-
linkedControlIds.push(control.id);
|
|
45
|
-
}
|
|
14
|
+
await upsertResource(loaded.root, plan.existingSystem, plan.system);
|
|
15
|
+
await updateResource(loaded.root, "workspace", plan.workspace.id, plan.workspace);
|
|
16
|
+
if (plan.renderer) await updateResource(loaded.root, plan.renderer.type, plan.renderer.id, plan.renderer);
|
|
46
17
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.filter(({ type, status }) => type === "framework" && status === "active")
|
|
55
|
-
.map(({ id }) => id),
|
|
56
|
-
requirementIds: current.resources
|
|
57
|
-
.filter(({ type, applicability }) => type === "requirement" && applicability === "applicable")
|
|
58
|
-
.map(({ id }) => id),
|
|
59
|
-
controlIds: current.resources
|
|
60
|
-
.filter(({ type, status }) => type === "control" && !["not-applicable", "retired"].includes(status))
|
|
61
|
-
.map(({ id }) => id),
|
|
62
|
-
systemIds: [...new Set([...(existingWorkspace.systemIds || []), systemId])]
|
|
18
|
+
return {
|
|
19
|
+
draft: setup.draft,
|
|
20
|
+
system: plan.system,
|
|
21
|
+
workspace: plan.workspace,
|
|
22
|
+
linkedControlIds: [],
|
|
23
|
+
evidenceTestDraftIds: [],
|
|
24
|
+
onboardingComplete: !setup.draft
|
|
63
25
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
current = await loadWorkspace(current.root);
|
|
67
|
-
const renderer = current.resources.find(({ type }) => type === "renderer-settings");
|
|
68
|
-
if (renderer) {
|
|
69
|
-
await updateResource(current.root, renderer.type, renderer.id, {
|
|
70
|
-
...renderer,
|
|
71
|
-
showOnboarding: setup.draft
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
const evidenceTestDrafts = setup.draft
|
|
75
|
-
? { created: [], existing: [], total: 0 }
|
|
76
|
-
: await ensureEvidenceTestDrafts(current.root);
|
|
26
|
+
}
|
|
77
27
|
|
|
28
|
+
export async function planWorkspaceSetup(input = process.cwd(), payload = {}) {
|
|
29
|
+
const loaded = await loadWorkspace(input);
|
|
30
|
+
const setup = normalizeSetupPayload(payload);
|
|
31
|
+
validateSetup(loaded, setup);
|
|
32
|
+
const plan = buildSetupRecords(loaded, setup);
|
|
78
33
|
return {
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
preview: true,
|
|
79
36
|
draft: setup.draft,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
37
|
+
changes: {
|
|
38
|
+
system: plan.existingSystem ? "update" : "create",
|
|
39
|
+
workspace: "update",
|
|
40
|
+
renderer: plan.renderer ? "update" : "unchanged",
|
|
41
|
+
controls: 0,
|
|
42
|
+
evidenceDrafts: 0
|
|
43
|
+
},
|
|
44
|
+
system: setupSystemSummary(plan.system),
|
|
45
|
+
target: setupTargetSummary(plan.workspace),
|
|
84
46
|
onboardingComplete: !setup.draft
|
|
85
47
|
};
|
|
86
48
|
}
|
|
87
49
|
|
|
50
|
+
export function summarizeSetupResult(result) {
|
|
51
|
+
return {
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
preview: false,
|
|
54
|
+
draft: result.draft,
|
|
55
|
+
changes: {
|
|
56
|
+
system: "saved",
|
|
57
|
+
workspace: "updated",
|
|
58
|
+
controls: result.linkedControlIds?.length || 0,
|
|
59
|
+
evidenceDrafts: result.evidenceTestDraftIds?.length || 0
|
|
60
|
+
},
|
|
61
|
+
system: setupSystemSummary(result.system),
|
|
62
|
+
target: setupTargetSummary(result.workspace),
|
|
63
|
+
onboardingComplete: result.onboardingComplete
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
88
67
|
export function normalizeSetupPayload(payload = {}) {
|
|
89
68
|
if (!payload || Array.isArray(payload) || typeof payload !== "object") {
|
|
90
69
|
throw new Error("Setup input must be a JSON object.");
|
|
@@ -146,6 +125,44 @@ function findSetupSystem(resources, setup) {
|
|
|
146
125
|
));
|
|
147
126
|
}
|
|
148
127
|
|
|
128
|
+
function buildSetupRecords(loaded, setup) {
|
|
129
|
+
const existingSystem = findSetupSystem(loaded.resources, setup);
|
|
130
|
+
const systemId = existingSystem?.id || createResourceId(
|
|
131
|
+
"system",
|
|
132
|
+
setup.serviceName,
|
|
133
|
+
loaded.resources.map(({ id }) => id)
|
|
134
|
+
);
|
|
135
|
+
const system = {
|
|
136
|
+
...(existingSystem || {}),
|
|
137
|
+
schemaVersion: 1,
|
|
138
|
+
id: systemId,
|
|
139
|
+
type: "system",
|
|
140
|
+
title: setup.serviceName,
|
|
141
|
+
status: setup.draft
|
|
142
|
+
? existingSystem?.status || "planned"
|
|
143
|
+
: existingSystem?.status === "planned"
|
|
144
|
+
? "active"
|
|
145
|
+
: existingSystem?.status || "active",
|
|
146
|
+
criticality: setup.criticality,
|
|
147
|
+
ownerIds: [setup.ownerId],
|
|
148
|
+
description: setup.boundary,
|
|
149
|
+
systemKind: existingSystem?.systemKind || "service",
|
|
150
|
+
dataClassification: setup.dataClassification,
|
|
151
|
+
internetExposed: setup.internetExposed,
|
|
152
|
+
inScope: true
|
|
153
|
+
};
|
|
154
|
+
const existingWorkspace = loaded.resources.find(({ type }) => type === "workspace");
|
|
155
|
+
if (!existingWorkspace) throw new Error("The workspace settings record was not found.");
|
|
156
|
+
const workspace = {
|
|
157
|
+
...existingWorkspace,
|
|
158
|
+
assuranceGoal: assuranceGoalFromSetup(setup.programGoal),
|
|
159
|
+
systemIds: [...new Set([...(existingWorkspace.systemIds || []), systemId])]
|
|
160
|
+
};
|
|
161
|
+
const existingRenderer = loaded.resources.find(({ type }) => type === "renderer-settings");
|
|
162
|
+
const renderer = existingRenderer ? { ...existingRenderer, showOnboarding: setup.draft } : null;
|
|
163
|
+
return { existingSystem, system, workspace, renderer };
|
|
164
|
+
}
|
|
165
|
+
|
|
149
166
|
async function upsertResource(root, existing, record) {
|
|
150
167
|
return existing
|
|
151
168
|
? updateResource(root, record.type, record.id, record)
|
|
@@ -159,6 +176,27 @@ function assuranceGoalFromSetup(goal) {
|
|
|
159
176
|
return "none";
|
|
160
177
|
}
|
|
161
178
|
|
|
179
|
+
function setupSystemSummary(system) {
|
|
180
|
+
return {
|
|
181
|
+
id: system.id,
|
|
182
|
+
title: system.title,
|
|
183
|
+
status: system.status,
|
|
184
|
+
inScope: system.inScope
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function setupTargetSummary(workspace) {
|
|
189
|
+
return {
|
|
190
|
+
assuranceGoal: workspace.assuranceGoal,
|
|
191
|
+
scopeCounts: {
|
|
192
|
+
systems: workspace.systemIds?.length || 0,
|
|
193
|
+
frameworks: workspace.frameworkIds?.length || 0,
|
|
194
|
+
requirements: workspace.requirementIds?.length || 0,
|
|
195
|
+
controls: workspace.controlIds?.length || 0
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
162
200
|
function booleanValue(value, name) {
|
|
163
201
|
if (value === true || value === false) return value;
|
|
164
202
|
if (value === "true") return true;
|