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.
- package/README.md +24 -6
- package/model/index.js +41 -3
- package/model/v1.json +81 -47
- package/model/v2.json +8022 -0
- package/model/v3.json +9391 -0
- package/package.json +2 -2
- package/src/agent.js +89 -8
- package/src/appointments.js +19 -0
- package/src/audit-preparation.js +109 -65
- package/src/audit-transition.js +96 -0
- package/src/batch-review.js +109 -0
- package/src/cli.js +563 -148
- package/src/collection-review.js +185 -0
- package/src/coverage.js +50 -0
- package/src/evidence-packet.js +149 -77
- package/src/external-reviewer.js +165 -0
- package/src/files.js +267 -29
- package/src/git.js +239 -41
- package/src/index.js +41 -7
- package/src/model-docs.js +103 -7
- package/src/model-migration.js +1958 -0
- package/src/mutation.js +42 -0
- package/src/obligations.js +502 -95
- package/src/parties.js +17 -2
- package/src/program-lifecycle.js +1 -0
- package/src/program-path.js +70 -60
- package/src/program-readiness.js +470 -130
- package/src/reconciliation.js +277 -0
- package/src/resource-status.js +17 -0
- package/src/server.js +347 -48
- package/src/setup.js +57 -26
- package/src/source-coverage.js +61 -0
- package/src/state.js +122 -25
- package/src/timing.js +41 -0
- package/src/validate.js +707 -44
- package/src/web.js +1440 -304
- package/src/workflow.js +1595 -0
- package/src/workspace.js +15 -7
- package/src/evidence-tests.js +0 -69
|
@@ -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
|
+
}
|