filegrc 0.3.4 → 0.5.0

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.
@@ -0,0 +1,96 @@
1
+ import { createResource } from "./files.js";
2
+ import { createResourceId } from "./id.js";
3
+ import { loadWorkspace } from "./workspace.js";
4
+
5
+ export async function planNextAuditCycle(input = process.cwd(), options = {}) {
6
+ const loaded = await loadWorkspace(input);
7
+ if (String(loaded.model.modelVersion) !== "3") {
8
+ throw new Error("Audit-cycle carry-forward requires a model v3 workspace.");
9
+ }
10
+ const prior = loaded.resources.find((record) => (
11
+ record.type === "audit" && record.id === options.priorAuditId
12
+ ));
13
+ if (!prior) throw new Error("A prior Audit record is required.");
14
+ const startsOn = required(options.startsOn, "Next period start");
15
+ const endsOn = required(options.endsOn, "Next period end");
16
+ if (startsOn > endsOn) throw new Error("The next period end must be on or after its start.");
17
+ const priorEnd = prior.coverage?.endsOn || prior.coverage?.on;
18
+ if (prior.auditKind === "soc-2-type-1" && priorEnd && startsOn <= priorEnd) {
19
+ throw new Error(`A Type 2 operating period must start after the Type 1 as-of date ${priorEnd}.`);
20
+ }
21
+ const title = String(options.title || nextTitle(prior, startsOn, endsOn)).trim();
22
+ const audit = {
23
+ id: options.id || createResourceId("audit", title, loaded.resources.map(({ id }) => id)),
24
+ type: "audit",
25
+ title,
26
+ status: "planned",
27
+ auditKind: "soc-2-type-2",
28
+ priorAuditId: prior.id,
29
+ coverage: { kind: "range", startsOn, endsOn },
30
+ frameworkIds: [...(prior.frameworkIds || [])],
31
+ systemIds: [...(prior.systemIds || [])],
32
+ requirementIds: [...(prior.requirementIds || [])],
33
+ controlIds: [...(prior.controlIds || [])],
34
+ complementaryControlIds: [...(prior.complementaryControlIds || [])],
35
+ subserviceVendorIds: [...(prior.subserviceVendorIds || [])],
36
+ ...(prior.subserviceMethod ? { subserviceMethod: prior.subserviceMethod } : {}),
37
+ ...(prior.complementaryControlsConclusion
38
+ ? { complementaryControlsConclusion: prior.complementaryControlsConclusion }
39
+ : {}),
40
+ ...(prior.auditorVendorId ? { auditorVendorId: prior.auditorVendorId } : {}),
41
+ contactIds: [...(prior.contactIds || [])],
42
+ ownerIds: [...(prior.ownerIds || [])],
43
+ signatoryAppointmentIds: [...(prior.signatoryAppointmentIds || [])],
44
+ scope: String(options.scope || prior.scope || "").trim(),
45
+ ...(String(options.scopeRevision || "").trim()
46
+ ? { scopeRevision: String(options.scopeRevision).trim() }
47
+ : {})
48
+ };
49
+ return {
50
+ operation: prior.auditKind === "soc-2-type-1" ? "type-1-to-type-2" : "next-audit-cycle",
51
+ priorAuditId: prior.id,
52
+ audit,
53
+ carriedForward: [
54
+ "frameworkIds",
55
+ "systemIds",
56
+ "requirementIds",
57
+ "controlIds",
58
+ "complementaryControlIds",
59
+ "subserviceVendorIds",
60
+ "subserviceMethod",
61
+ "auditorVendorId",
62
+ "contactIds",
63
+ "ownerIds",
64
+ "signatoryAppointmentIds",
65
+ "scope"
66
+ ],
67
+ reviewRequired: [
68
+ "coverage",
69
+ "scopeRevision",
70
+ "criteria and control changes since the prior audit",
71
+ "source and policy continuity",
72
+ "subservice assurance coverage",
73
+ "new or changed commitments"
74
+ ]
75
+ };
76
+ }
77
+
78
+ export async function createNextAuditCycle(input = process.cwd(), options = {}) {
79
+ if (options.confirmed !== true) {
80
+ throw new Error("Preview the next audit cycle and confirm the write.");
81
+ }
82
+ const plan = await planNextAuditCycle(input, options);
83
+ const result = await createResource(input, plan.audit);
84
+ return { ...plan, result };
85
+ }
86
+
87
+ function required(value, label) {
88
+ const normalized = String(value || "").trim();
89
+ if (!normalized) throw new Error(`${label} is required.`);
90
+ return normalized;
91
+ }
92
+
93
+ function nextTitle(prior, startsOn, endsOn) {
94
+ const year = endsOn.slice(0, 4) || startsOn.slice(0, 4);
95
+ return `${year} SOC 2 Type 2 audit after ${prior.title}`;
96
+ }
@@ -0,0 +1,109 @@
1
+ import { applyResourceBatch } from "./files.js";
2
+ import { getGitSummary } from "./git.js";
3
+ import { loadWorkspace } from "./workspace.js";
4
+
5
+ const REVIEWABLE_TYPES = new Set([
6
+ "requirement",
7
+ "control",
8
+ "commitment",
9
+ "complementary-control"
10
+ ]);
11
+
12
+ export async function scaffoldApplicabilityReview(input = process.cwd(), options = {}) {
13
+ const loaded = await loadWorkspace(input);
14
+ if (String(loaded.model.modelVersion) !== "3") {
15
+ throw new Error("Batch applicability review requires a model v3 workspace.");
16
+ }
17
+ const requestedType = options.type ? String(options.type) : null;
18
+ if (requestedType && !REVIEWABLE_TYPES.has(requestedType)) {
19
+ throw new Error(`Applicability review type must be one of ${[...REVIEWABLE_TYPES].join(", ")}.`);
20
+ }
21
+ const records = loaded.resources.filter((record) => (
22
+ REVIEWABLE_TYPES.has(record.type)
23
+ && (!requestedType || record.type === requestedType)
24
+ && !record.applicabilityReview
25
+ && !["retired", "superseded"].includes(record.status)
26
+ ));
27
+ return {
28
+ reviewedByIds: [],
29
+ reviewedOn: null,
30
+ decisions: records
31
+ .sort((left, right) => `${left.type}:${left.title}:${left.id}`.localeCompare(`${right.type}:${right.title}:${right.id}`))
32
+ .map((record) => ({
33
+ id: record.id,
34
+ decision: null,
35
+ rationale: null
36
+ }))
37
+ };
38
+ }
39
+
40
+ export async function planApplicabilityReview(input = process.cwd(), options = {}) {
41
+ const loaded = await loadWorkspace(input);
42
+ if (String(loaded.model.modelVersion) !== "3") {
43
+ throw new Error("Batch applicability review requires a model v3 workspace.");
44
+ }
45
+ if (!Array.isArray(options.decisions) || !options.decisions.length) {
46
+ throw new Error("Applicability review needs at least one decision.");
47
+ }
48
+ const byId = new Map(loaded.resources.map((record) => [record.id, record]));
49
+ const update = options.decisions.map((decision) => {
50
+ const record = byId.get(decision.id);
51
+ if (!record || !REVIEWABLE_TYPES.has(record.type)) {
52
+ throw new Error(`Resource "${decision.id}" is not an applicability-review record.`);
53
+ }
54
+ const reviewedByIds = [...new Set((decision.reviewedByIds || options.reviewedByIds || []).map(String))];
55
+ const reviewedOn = String(decision.reviewedOn || options.reviewedOn || "").trim();
56
+ const scopeRevision = String(
57
+ decision.scopeRevision
58
+ || options.scopeRevision
59
+ || getGitSummary(loaded.root).commit
60
+ || "uncommitted"
61
+ ).trim();
62
+ const rationale = String(decision.rationale || "").trim();
63
+ const result = String(decision.decision || "").trim();
64
+ if (!["applicable", "not-applicable", "externally-managed", "zero-population"].includes(result)) {
65
+ throw new Error(`Decision for "${record.id}" must be applicable, not-applicable, externally-managed, or zero-population.`);
66
+ }
67
+ if (!reviewedByIds.length || !reviewedOn || !rationale) {
68
+ throw new Error(`Decision for "${record.id}" needs a reviewer, review date, and rationale.`);
69
+ }
70
+ const next = {
71
+ ...record,
72
+ applicabilityReview: {
73
+ decision: result,
74
+ rationale,
75
+ reviewedByIds,
76
+ reviewedOn,
77
+ scopeRevision
78
+ }
79
+ };
80
+ if (record.type === "requirement") {
81
+ if (!["applicable", "not-applicable"].includes(result)) {
82
+ throw new Error(`Requirement "${record.id}" must be applicable or not-applicable.`);
83
+ }
84
+ next.applicability = result;
85
+ next.applicabilityRationale = rationale;
86
+ }
87
+ if (record.type === "control" && result === "not-applicable") next.status = "not-applicable";
88
+ if (record.type === "control" && result === "applicable" && record.status === "not-applicable") next.status = "planned";
89
+ return next;
90
+ });
91
+ return {
92
+ operation: "applicability-review",
93
+ reviewedIds: update.map(({ id }) => id),
94
+ changes: {
95
+ update,
96
+ expectedRevisions: options.expectedRevisions || {},
97
+ validateWholeWorkspace: true
98
+ }
99
+ };
100
+ }
101
+
102
+ export async function applyApplicabilityReview(input = process.cwd(), options = {}) {
103
+ if (options.confirmed !== true) {
104
+ throw new Error("Preview the applicability decisions and confirm the write.");
105
+ }
106
+ const plan = await planApplicabilityReview(input, options);
107
+ const result = await applyResourceBatch(input, plan.changes);
108
+ return { ...plan, result };
109
+ }