filegrc 0.6.4 → 0.7.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/model/v4.json +82 -72
- package/package.json +1 -1
- package/src/batch-review.js +142 -17
- package/src/cli.js +66 -2
- package/src/collection-review.js +15 -26
- package/src/collection-revision.js +83 -0
- package/src/collection-scope.js +160 -2
- package/src/content-readiness.js +11 -0
- package/src/files.js +5 -0
- package/src/git.js +54 -5
- package/src/index.js +1 -0
- package/src/policy-activation.js +84 -0
- package/src/program-lifecycle.js +4 -0
- package/src/program-path.js +11 -10
- package/src/program-readiness.js +237 -37
- package/src/reconciliation.js +3 -1
- package/src/server.js +12 -3
- package/src/validate.js +35 -20
- package/src/web.js +150 -23
- package/src/workflow.js +27 -1
package/src/collection-scope.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { programComponents } from "./program.js";
|
|
1
|
+
import { programComponents, selectedRequirementIds } from "./program.js";
|
|
2
2
|
|
|
3
3
|
export function scopedCollectionRecords(loaded, resourceType, program) {
|
|
4
4
|
if (String(loaded.model.modelVersion) !== "4") {
|
|
@@ -10,10 +10,22 @@ export function scopedCollectionRecords(loaded, resourceType, program) {
|
|
|
10
10
|
const scopedProgram = program || {};
|
|
11
11
|
const components = programComponents(loaded, scopedProgram);
|
|
12
12
|
const componentIds = new Set(components.map(({ id }) => id));
|
|
13
|
+
const systemIds = new Set(scopedProgram.systemIds || []);
|
|
14
|
+
const controlIds = new Set(scopedProgram.controlIds || []);
|
|
13
15
|
const selected = {
|
|
14
|
-
system:
|
|
16
|
+
system: systemIds,
|
|
15
17
|
component: componentIds,
|
|
16
18
|
framework: new Set(scopedProgram.frameworkIds || []),
|
|
19
|
+
"complementary-control": new Set(loaded.resources.filter((record) => (
|
|
20
|
+
record.type === "complementary-control"
|
|
21
|
+
&& record.status !== "superseded"
|
|
22
|
+
&& record.status !== "retired"
|
|
23
|
+
&& (
|
|
24
|
+
(record.systemIds || []).some((id) => systemIds.has(id))
|
|
25
|
+
|| (record.relatedControlIds || []).some((id) => controlIds.has(id))
|
|
26
|
+
|| (record.componentIds || []).some((id) => componentIds.has(id))
|
|
27
|
+
)
|
|
28
|
+
)).map(({ id }) => id)),
|
|
17
29
|
asset: new Set(loaded.resources.filter((record) => (
|
|
18
30
|
record.type === "asset" && (record.componentIds || []).some((id) => componentIds.has(id))
|
|
19
31
|
)).map(({ id }) => id))
|
|
@@ -22,3 +34,149 @@ export function scopedCollectionRecords(loaded, resourceType, program) {
|
|
|
22
34
|
record.type === resourceType && (!selected || selected.has(record.id))
|
|
23
35
|
));
|
|
24
36
|
}
|
|
37
|
+
|
|
38
|
+
export function collectionRevisionInputs(loaded, resourceType, program) {
|
|
39
|
+
const reviewed = scopedCollectionRecords(loaded, resourceType, program);
|
|
40
|
+
if (String(loaded.model.modelVersion) !== "4") {
|
|
41
|
+
return reviewed.map((record) => ({ record, value: record }));
|
|
42
|
+
}
|
|
43
|
+
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
44
|
+
const records = new Map(reviewed.map((record) => [record.id, record]));
|
|
45
|
+
const reviewedIds = new Set(records.keys());
|
|
46
|
+
const addIds = (ids) => {
|
|
47
|
+
for (const id of ids || []) {
|
|
48
|
+
const record = byId.get(id);
|
|
49
|
+
if (record) records.set(record.id, record);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (resourceType === "framework") {
|
|
54
|
+
addIds(program?.systemIds);
|
|
55
|
+
addIds(selectedRequirementIds(program || {}, loaded.model));
|
|
56
|
+
}
|
|
57
|
+
if (resourceType === "vendor") {
|
|
58
|
+
for (const record of reviewed) {
|
|
59
|
+
addIds([record.agreementDocumentId, record.classificationId]);
|
|
60
|
+
addIds(record.informationTypeIds);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (resourceType === "system") {
|
|
64
|
+
for (const record of reviewed) {
|
|
65
|
+
addIds([record.classificationId]);
|
|
66
|
+
addIds(record.informationTypeIds);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (resourceType === "component") {
|
|
70
|
+
addIds(program?.systemIds);
|
|
71
|
+
addIds(program?.controlIds);
|
|
72
|
+
for (const record of reviewed) {
|
|
73
|
+
addIds([record.vendorId, record.classificationId]);
|
|
74
|
+
addIds((record.informationUses || []).map(({ informationTypeId }) => informationTypeId));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (resourceType === "complementary-control") {
|
|
78
|
+
addIds(program?.systemIds);
|
|
79
|
+
addIds(program?.controlIds);
|
|
80
|
+
for (const record of reviewed) {
|
|
81
|
+
addIds([record.vendorId]);
|
|
82
|
+
addIds(record.requirementIds);
|
|
83
|
+
addIds(record.commitmentIds);
|
|
84
|
+
addIds(record.sourceDocumentIds);
|
|
85
|
+
addIds(record.componentIds);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [...records.values()].map((record) => ({
|
|
89
|
+
record,
|
|
90
|
+
value: reviewedIds.has(record.id)
|
|
91
|
+
? record
|
|
92
|
+
: dependencyRevisionValue(resourceType, record)
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function collectionScopeRevisionFacts(loaded, resourceType, program) {
|
|
97
|
+
if (String(loaded.model.modelVersion) !== "4") {
|
|
98
|
+
const common = { programId: program?.id ?? null };
|
|
99
|
+
if (resourceType === "framework") {
|
|
100
|
+
return {
|
|
101
|
+
...common,
|
|
102
|
+
assuranceGoal: program?.assuranceGoal ?? null,
|
|
103
|
+
systemIds: sorted(program?.systemIds),
|
|
104
|
+
frameworkIds: sorted(program?.frameworkIds),
|
|
105
|
+
requirementIds: sorted(selectedRequirementIds(program || {}, loaded.model))
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (resourceType === "vendor" || resourceType === "system" || resourceType === "complementary-control") {
|
|
109
|
+
return {
|
|
110
|
+
...common,
|
|
111
|
+
systemIds: sorted(program?.systemIds),
|
|
112
|
+
controlIds: sorted(program?.controlIds)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return common;
|
|
116
|
+
}
|
|
117
|
+
const common = { programId: program?.id ?? null };
|
|
118
|
+
if (resourceType === "framework") {
|
|
119
|
+
return {
|
|
120
|
+
...common,
|
|
121
|
+
assuranceGoal: program?.assuranceGoal ?? null,
|
|
122
|
+
systemIds: sorted(program?.systemIds),
|
|
123
|
+
frameworkIds: sorted(program?.frameworkIds),
|
|
124
|
+
requirementIds: sorted(selectedRequirementIds(program || {}, loaded.model))
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (resourceType === "system") {
|
|
128
|
+
return { ...common, systemIds: sorted(program?.systemIds) };
|
|
129
|
+
}
|
|
130
|
+
if (resourceType === "component" || resourceType === "complementary-control") {
|
|
131
|
+
return {
|
|
132
|
+
...common,
|
|
133
|
+
systemIds: sorted(program?.systemIds),
|
|
134
|
+
controlIds: sorted(program?.controlIds)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return common;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const dependencyFields = {
|
|
141
|
+
framework: {
|
|
142
|
+
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"],
|
|
143
|
+
requirement: ["id", "type", "title", "frameworkId", "reference", "description", "parentRequirementId"]
|
|
144
|
+
},
|
|
145
|
+
vendor: {
|
|
146
|
+
document: ["id", "type", "status", "documentKind", "version", "effectiveOn", "approvedOn", "approvedContentRevisions", "systemIds", "controlIds", "componentIds", "classificationId"],
|
|
147
|
+
classification: ["id", "type", "status", "rank", "description", "handlingRequirements"],
|
|
148
|
+
"information-type": ["id", "type", "status", "classificationId", "description"]
|
|
149
|
+
},
|
|
150
|
+
system: {
|
|
151
|
+
classification: ["id", "type", "status", "rank", "description", "handlingRequirements"],
|
|
152
|
+
"information-type": ["id", "type", "status", "classificationId", "description"]
|
|
153
|
+
},
|
|
154
|
+
component: {
|
|
155
|
+
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "criticality", "informationTypeIds", "classificationId", "internetExposed", "continuityObjectives"],
|
|
156
|
+
control: ["id", "type", "status", "statement", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
157
|
+
vendor: ["id", "type", "status", "category", "criticality", "description", "standardAgreement", "agreementDocumentId", "startDate", "endDate", "classificationId", "informationTypeIds"],
|
|
158
|
+
classification: ["id", "type", "status", "rank", "description", "handlingRequirements"],
|
|
159
|
+
"information-type": ["id", "type", "status", "classificationId", "description"]
|
|
160
|
+
},
|
|
161
|
+
"complementary-control": {
|
|
162
|
+
system: ["id", "type", "status", "purpose", "servicesProvided", "boundary", "exclusions", "informationTypeIds", "classificationId", "internetExposed"],
|
|
163
|
+
control: ["id", "type", "status", "statement", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "componentIds", "evidenceSourceComponentIds"],
|
|
164
|
+
vendor: ["id", "type", "status", "category", "criticality", "description", "standardAgreement", "agreementDocumentId", "startDate", "endDate", "classificationId", "informationTypeIds"],
|
|
165
|
+
requirement: ["id", "type", "title", "frameworkId", "reference", "description", "parentRequirementId"],
|
|
166
|
+
commitment: ["id", "type", "status", "commitmentKind", "statement", "systemIds", "requirementIds", "controlIds", "customerFacing", "effectiveOn"],
|
|
167
|
+
document: ["id", "type", "status", "documentKind", "version", "effectiveOn", "approvedOn", "approvedContentRevisions", "systemIds", "controlIds", "componentIds", "classificationId"],
|
|
168
|
+
component: ["id", "type", "status", "componentKind", "description", "vendorId", "systemUses", "informationUses", "internetExposed"]
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
function dependencyRevisionValue(resourceType, record) {
|
|
173
|
+
const fields = dependencyFields[resourceType]?.[record.type];
|
|
174
|
+
if (!fields) return { id: record.id, type: record.type };
|
|
175
|
+
return Object.fromEntries(fields
|
|
176
|
+
.filter((field) => record[field] !== undefined)
|
|
177
|
+
.map((field) => [field, record[field]]));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sorted(values) {
|
|
181
|
+
return [...(values || [])].sort();
|
|
182
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function openPlaceholderCount(source) {
|
|
2
|
+
if (!source) return 0;
|
|
3
|
+
const matches = source.match(
|
|
4
|
+
/\{\{[^}\n]+\}\}|\b(?:TODO|TBD)\b|\[(?:complete|confirm|describe|insert|name|replace|select|specify|todo|tbd)[^\]\n]*\]/giu
|
|
5
|
+
);
|
|
6
|
+
return matches?.length || 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function substantiveMarkdown(source) {
|
|
10
|
+
return (source.match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) || []).length >= 10;
|
|
11
|
+
}
|
package/src/files.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { constants, link, lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { getResourceDefinition, loadModel } from "../model/index.js";
|
|
5
|
+
import { openPlaceholderCount } from "./content-readiness.js";
|
|
5
6
|
import { serializeWorkspaceMutation, workspaceValidationDeferred } from "./mutation.js";
|
|
6
7
|
import { isCanonicalDataPath, resolveDataPath, resolveWorkspaceRoot } from "./paths.js";
|
|
7
8
|
import { markdownEntries } from "./resource-markdown.js";
|
|
@@ -755,6 +756,10 @@ async function prepareApprovalBinding(loaded, record, contentWrites, previousRec
|
|
|
755
756
|
throw error;
|
|
756
757
|
}
|
|
757
758
|
}
|
|
759
|
+
const placeholders = openPlaceholderCount(source);
|
|
760
|
+
if (placeholders) {
|
|
761
|
+
throw new Error(`Cannot approve or activate ${record.title} while its ${item.label} Markdown contains ${placeholders} open ${placeholders === 1 ? "placeholder" : "placeholders"}. Complete the facts and review the exact content first.`);
|
|
762
|
+
}
|
|
758
763
|
revisions[item.path] = contentRevision(source);
|
|
759
764
|
}
|
|
760
765
|
nextRecord[bindingField] = revisions;
|
package/src/git.js
CHANGED
|
@@ -7,7 +7,7 @@ import { performance } from "node:perf_hooks";
|
|
|
7
7
|
import { isSafeGitName } from "./git-name.js";
|
|
8
8
|
import { serializeWorkspaceMutation, withDeferredWorkspaceValidation } from "./mutation.js";
|
|
9
9
|
import { resolveWorkspaceRoot } from "./paths.js";
|
|
10
|
-
import { measureTiming, recordTiming, timingEnabled } from "./timing.js";
|
|
10
|
+
import { measureTiming, measureTimingSync, recordTiming, timingEnabled } from "./timing.js";
|
|
11
11
|
import { fingerprintWorkspace, validateWorkspace } from "./validate.js";
|
|
12
12
|
import { loadWorkspace } from "./workspace.js";
|
|
13
13
|
|
|
@@ -44,7 +44,7 @@ export class GitOperationError extends Error {
|
|
|
44
44
|
export function getGitSummary(input = process.cwd()) {
|
|
45
45
|
const root = resolveWorkspaceRoot(input);
|
|
46
46
|
try {
|
|
47
|
-
const topLevel = git(root, ["rev-parse", "--show-toplevel"]);
|
|
47
|
+
const topLevel = measureTimingSync("git-discovery", () => git(root, ["rev-parse", "--show-toplevel"]));
|
|
48
48
|
const status = git(root, ["status", "--porcelain=v1", "--", "."]);
|
|
49
49
|
const commit = tryGit(root, ["rev-parse", "HEAD"]) || null;
|
|
50
50
|
const branch = tryGit(root, ["symbolic-ref", "--short", "HEAD"]) || null;
|
|
@@ -181,12 +181,40 @@ export function getRepositorySnapshot(input = process.cwd(), options = {}) {
|
|
|
181
181
|
return snapshot;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
export async function getWorkspaceRevisionSnapshot(input = process.cwd()) {
|
|
185
|
+
const root = resolveWorkspaceRoot(input);
|
|
186
|
+
try {
|
|
187
|
+
const source = await measureTiming("repository-revision", () => runGitCommand(root, [
|
|
188
|
+
"status",
|
|
189
|
+
"--porcelain=v2",
|
|
190
|
+
"--branch",
|
|
191
|
+
"-z",
|
|
192
|
+
"--untracked-files=all",
|
|
193
|
+
"--",
|
|
194
|
+
"data"
|
|
195
|
+
], { operation: "resolve the workspace revision" }));
|
|
196
|
+
const parsed = parseWorkspaceRevision(source);
|
|
197
|
+
return {
|
|
198
|
+
available: true,
|
|
199
|
+
commit: parsed.commit,
|
|
200
|
+
shortCommit: parsed.commit?.slice(0, 8) ?? "no commits",
|
|
201
|
+
clean: parsed.changePaths.length === 0,
|
|
202
|
+
changes: parsed.changePaths,
|
|
203
|
+
workspaceChangePaths: parsed.changePaths
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return unavailableSnapshot(error, { workspaceChangePaths: [] });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
184
210
|
async function buildRepositorySnapshot(root) {
|
|
185
211
|
let repositoryPaths;
|
|
186
212
|
try {
|
|
187
|
-
repositoryPaths = await
|
|
188
|
-
|
|
189
|
-
|
|
213
|
+
repositoryPaths = await measureTiming("git-discovery", () => runGitCommand(
|
|
214
|
+
root,
|
|
215
|
+
["rev-parse", "--show-toplevel", "--absolute-git-dir"],
|
|
216
|
+
{ operation: "locate the repository" }
|
|
217
|
+
));
|
|
190
218
|
} catch (error) {
|
|
191
219
|
return unavailableSnapshot(error);
|
|
192
220
|
}
|
|
@@ -1058,6 +1086,27 @@ function parsePorcelainV2(source, topLevel, root) {
|
|
|
1058
1086
|
return { commit, branch, upstream, ahead, behind, allChanges, workspaceChanges };
|
|
1059
1087
|
}
|
|
1060
1088
|
|
|
1089
|
+
function parseWorkspaceRevision(source) {
|
|
1090
|
+
const fields = source.split("\0").filter(Boolean);
|
|
1091
|
+
let commit = null;
|
|
1092
|
+
const changePaths = [];
|
|
1093
|
+
for (let index = 0; index < fields.length; index += 1) {
|
|
1094
|
+
const field = fields[index];
|
|
1095
|
+
if (field.startsWith("# branch.oid ")) {
|
|
1096
|
+
commit = field.slice(13) === "(initial)" ? null : field.slice(13);
|
|
1097
|
+
} else if (/^[12u?!] /.test(field)) {
|
|
1098
|
+
changePaths.push(porcelainV2Path(field));
|
|
1099
|
+
if (field.startsWith("2 ")) {
|
|
1100
|
+
changePaths.push(fields[++index]);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return {
|
|
1105
|
+
commit,
|
|
1106
|
+
changePaths: [...new Set(changePaths.filter(Boolean))].sort()
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1061
1110
|
function porcelainV2Path(field) {
|
|
1062
1111
|
if (field.startsWith("? ") || field.startsWith("! ")) return field.slice(2);
|
|
1063
1112
|
const requiredSpaces = field.startsWith("2 ") ? 9 : field.startsWith("u ") ? 10 : 8;
|
package/src/index.js
CHANGED
|
@@ -51,6 +51,7 @@ export {
|
|
|
51
51
|
export { generateModelDocumentation } from "./model-docs.js";
|
|
52
52
|
export { renderMarkdown } from "./markdown.js";
|
|
53
53
|
export { migrateModel, planModelMigration } from "./model-migration.js";
|
|
54
|
+
export { activatePolicies, planPolicyActivation, scaffoldPolicyActivation } from "./policy-activation.js";
|
|
54
55
|
export {
|
|
55
56
|
completeObligationAction,
|
|
56
57
|
completeObligationEvent,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { applyResourceBatch, contentRevision } from "./files.js";
|
|
2
|
+
import { serializeWorkspaceMutation } from "./mutation.js";
|
|
3
|
+
import { assessProgramReadiness } from "./program-readiness.js";
|
|
4
|
+
import { currentCalendarDate } from "./time.js";
|
|
5
|
+
import { loadWorkspace } from "./workspace.js";
|
|
6
|
+
|
|
7
|
+
export async function scaffoldPolicyActivation(input = process.cwd(), options = {}) {
|
|
8
|
+
const loaded = await loadWorkspace(input);
|
|
9
|
+
const readiness = await assessProgramReadiness(loaded, { programId: options.programId });
|
|
10
|
+
const approved = readiness.policyActivations.filter(({ state }) => (
|
|
11
|
+
["approved-implementation-pending", "ready-to-activate"].includes(state)
|
|
12
|
+
));
|
|
13
|
+
const revisionById = new Map(loaded.entries.map((entry) => [entry.record.id, contentRevision(entry.source)]));
|
|
14
|
+
return {
|
|
15
|
+
policyIds: approved.map(({ policyId }) => policyId),
|
|
16
|
+
effectiveOn: currentCalendarDate(loaded.workspace.timezone),
|
|
17
|
+
expectedRevisions: Object.fromEntries(approved.map(({ policyId }) => [policyId, revisionById.get(policyId)])),
|
|
18
|
+
confirmed: false
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function planPolicyActivation(input = process.cwd(), options = {}) {
|
|
23
|
+
const loaded = await loadWorkspace(input);
|
|
24
|
+
const policyIds = [...new Set((options.policyIds || []).map(String))];
|
|
25
|
+
if (!policyIds.length) throw new Error("Policy activation needs at least one approved Policy.");
|
|
26
|
+
const effectiveOn = String(options.effectiveOn || "").trim();
|
|
27
|
+
if (!isCalendarDate(effectiveOn)) throw new Error("Policy activation needs a real effective date in YYYY-MM-DD format.");
|
|
28
|
+
const today = currentCalendarDate(loaded.workspace.timezone);
|
|
29
|
+
if (effectiveOn < today) {
|
|
30
|
+
throw new Error(`The effective date ${effectiveOn} has passed. Choose ${today} or a future date; do not backdate adoption.`);
|
|
31
|
+
}
|
|
32
|
+
const expectedRevisions = options.expectedRevisions || {};
|
|
33
|
+
if (Array.isArray(expectedRevisions) || !expectedRevisions || typeof expectedRevisions !== "object") {
|
|
34
|
+
throw new Error("Policy activation expected revisions must be keyed by Policy ID.");
|
|
35
|
+
}
|
|
36
|
+
const entryById = new Map(loaded.entries.map((entry) => [entry.record.id, entry]));
|
|
37
|
+
const update = policyIds.map((policyId) => {
|
|
38
|
+
const entry = entryById.get(policyId);
|
|
39
|
+
if (!entry || entry.record.type !== "policy") throw new Error(`Policy "${policyId}" was not found.`);
|
|
40
|
+
if (entry.record.status !== "approved") {
|
|
41
|
+
throw new Error(`Policy "${policyId}" must be approved and inactive before the Step 3 cutover.`);
|
|
42
|
+
}
|
|
43
|
+
if (!/^[a-f0-9]{64}$/.test(expectedRevisions[policyId] || "")) {
|
|
44
|
+
throw new Error(`Policy activation needs the current revision for "${policyId}". Regenerate the cutover review and try again.`);
|
|
45
|
+
}
|
|
46
|
+
const record = { ...entry.record, status: "active", effectiveOn };
|
|
47
|
+
delete record.proposedEffectiveOn;
|
|
48
|
+
return record;
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
operation: "policy-activation",
|
|
52
|
+
policyIds,
|
|
53
|
+
effectiveOn,
|
|
54
|
+
changes: {
|
|
55
|
+
update,
|
|
56
|
+
expectedRevisions: Object.fromEntries(policyIds.map((policyId) => [
|
|
57
|
+
policyId,
|
|
58
|
+
expectedRevisions[policyId]
|
|
59
|
+
])),
|
|
60
|
+
validateWholeWorkspace: true
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function activatePolicies(input = process.cwd(), options = {}) {
|
|
66
|
+
if (options.confirmed !== true) throw new Error("Review the Policy activation cutover and confirm the write.");
|
|
67
|
+
return serializeWorkspaceMutation(input, async (root) => {
|
|
68
|
+
const plan = await planPolicyActivation(root, options);
|
|
69
|
+
const result = await applyResourceBatch(root, plan.changes);
|
|
70
|
+
return { ...plan, result };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isCalendarDate(value) {
|
|
75
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
76
|
+
if (!match) return false;
|
|
77
|
+
const [, year, month, day] = match.map(Number);
|
|
78
|
+
const date = new Date(0);
|
|
79
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
80
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
81
|
+
return date.getUTCFullYear() === year
|
|
82
|
+
&& date.getUTCMonth() === month - 1
|
|
83
|
+
&& date.getUTCDate() === day;
|
|
84
|
+
}
|
package/src/program-lifecycle.js
CHANGED
|
@@ -24,3 +24,7 @@ export function obligationIsRunning(obligation, byId, asOf) {
|
|
|
24
24
|
&& obligation.status === "active"
|
|
25
25
|
&& obligationProgramStatus(obligation, byId, asOf) === "accepted";
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
export function obligationIsEnabled(obligation) {
|
|
29
|
+
return obligation?.type === "obligation" && obligation.status === "active";
|
|
30
|
+
}
|
package/src/program-path.js
CHANGED
|
@@ -11,7 +11,7 @@ export const RESOURCE_INSTRUCTIONS = {
|
|
|
11
11
|
framework: "Confirm the criteria framework and version used for the program.",
|
|
12
12
|
requirement: "Keep the published criterion as catalog content. Record management applicability and rationale on the selected Program.",
|
|
13
13
|
commitment: "Record supplemental customer promises and service requirements that shape the scope or control design. The Commitment’s systemIds and controlIds are authoritative for what fulfills it.",
|
|
14
|
-
policy: "Tailor each
|
|
14
|
+
policy: "Tailor each Policy to match what the company is committing to. Clear placeholders, assign an owner and separate approver, then bind approval to the reviewed content. Approval does not prove implementation. Activate the Policy during the Step 3 cutover after reviewing its implementation gaps.",
|
|
15
15
|
document: "Tailor the governed plans and other supporting documents the program needs. Assign owners and approvers, then keep the approved Markdown in Git.",
|
|
16
16
|
control: "Finish each applicable starter Control with the procedure people follow, its owner, bounded System scope, operating Components, authoritative evidence-source Components, governing Policy and Requirement mappings, and implementation date. Put calendar and event schedules in Obligations.",
|
|
17
17
|
"complementary-control": "Review whether any in-scope Control depends on a customer or carved-out provider action. Record each real dependency, or confirm that the current scope has none.",
|
|
@@ -58,7 +58,7 @@ export const RESOURCE_PAGE_SUMMARIES = {
|
|
|
58
58
|
component: "Connect each material Component to a System.",
|
|
59
59
|
classification: "Define handling levels.",
|
|
60
60
|
"information-type": "Define information categories.",
|
|
61
|
-
policy: "
|
|
61
|
+
policy: "Tailor the starter Policy and have someone other than its owner approve it.",
|
|
62
62
|
document: "Adapt and approve plans.",
|
|
63
63
|
control: "Describe each Control and its evidence source.",
|
|
64
64
|
"complementary-control": "Record customer or provider responsibilities, or confirm there are none.",
|
|
@@ -77,7 +77,7 @@ export const PROGRAM_PATH = [
|
|
|
77
77
|
summary: "Name the owners, criteria, service, Systems, and providers in scope.",
|
|
78
78
|
sections: [
|
|
79
79
|
{ id: "ownership", title: "Program Ownership", description: "Confirm the people, appointments, and teams that own, approve, review, and operate the program.", steps: ["Confirm the initial program lead’s actual job title and the separate Policy Owner Appointment.", "Add the organization’s real appointments, reviewers, and operators.", "Review the starter Security and Risk Oversight team, its members, and its chair.", "Add other teams only when the organization assigns shared responsibility to them."], types: ["person", "appointment", "team"], defaultOpen: true },
|
|
80
|
-
{ id: "criteria", title: "Program and Criteria", description: "Define the Program, confirm its Frameworks, record Program-scoped Requirement applicability, and connect customer commitments that shape the System or Control design.", steps: ["Confirm the Program goal, owners, risk method, and candidate period.", "Review the included Security criteria references and record each applicability decision on the Program.", "Record customer commitments and keep optional criteria out until
|
|
80
|
+
{ id: "criteria", title: "Program and Criteria", description: "Define the Program, confirm its Frameworks, record Program-scoped Requirement applicability, and connect customer commitments that shape the System or Control design.", steps: ["Confirm the Program goal, owners, risk method, and candidate period.", "Review the included Security criteria references and record each applicability decision on the Program.", "Record customer commitments and keep optional criteria out until the company chooses to add them."], types: ["program", "framework", "requirement", "commitment"], defaultOpen: true },
|
|
81
81
|
{ id: "boundary", title: "System Boundary", description: "Start with the bounded System. Add Components that materially deliver the service, support Controls, produce authoritative Evidence, or support relevant operations. Keep Vendor relationships and specific Assets separate.", steps: ["Create the complete bounded System and select it on the Program.", "Add only relevant Components, with a role and rationale for each System use.", "Create Vendors for material external provider relationships and link supplied Components when factual.", "Normalize Information Types and Classifications used by the System, Components, Vendors, Risks, and Evidence Artifacts."], types: ["system", "component", "vendor", "classification", "information-type"], defaultOpen: false }
|
|
82
82
|
],
|
|
83
83
|
resourceTypes: ["person", "appointment", "team", "program", "framework", "requirement", "commitment", "system", "component", "vendor", "classification", "information-type"],
|
|
@@ -99,12 +99,12 @@ export const PROGRAM_PATH = [
|
|
|
99
99
|
id: "policies",
|
|
100
100
|
number: 2,
|
|
101
101
|
title: "Approve Policies",
|
|
102
|
-
description: "Tailor, review,
|
|
102
|
+
description: "Tailor, review, and approve",
|
|
103
103
|
summary: "Adapt and approve the starter policies.",
|
|
104
104
|
sections: [
|
|
105
|
-
{ id: "library", title: "Policy Library", description: "Review
|
|
105
|
+
{ id: "library", title: "Policy Library", description: "Review and approve Policy requirements without treating approval as proof of technical implementation.", steps: ["Review Policy Markdown and replace every organization placeholder.", "Confirm the owner, separate approver, audience, review Obligation, and Controls that point to the Policy.", "Record approval against the exact reviewed content. Leave the Policy approved and inactive until the Step 3 implementation cutover."], types: ["policy"], defaultOpen: true }
|
|
106
106
|
],
|
|
107
|
-
resourceTypes: ["policy"
|
|
107
|
+
resourceTypes: ["policy"],
|
|
108
108
|
commands: [
|
|
109
109
|
"filegrc guide policy --json",
|
|
110
110
|
"filegrc list policy --json",
|
|
@@ -118,14 +118,15 @@ export const PROGRAM_PATH = [
|
|
|
118
118
|
description: "Finish controls and their evidence sources",
|
|
119
119
|
summary: "Describe each Control and connect its evidence source.",
|
|
120
120
|
sections: [
|
|
121
|
-
{ id: "catalog", title: "Control Catalog", description: "Finish the starter Controls and
|
|
121
|
+
{ id: "catalog", title: "Control Catalog", description: "Finish the starter Controls, governed plans, schedules, and authoritative evidence sources, then review the approved Policies together at implementation cutover.", steps: ["Open every planned Control and confirm its mappings and operation pattern.", "Write the real procedure in Record Markdown, add bounded System scope, and map the operating and authoritative evidence-source Components.", "Create or enable every calendar and event schedule as an Obligation. Enabled work remains dormant until its governing Policy is active.", "Confirm each source Component is active, has an evidence-source role and rationale in the Control's System scope, has current access owners, and includes repeatable retrieval instructions in Record Markdown.", "Complete required governed plans, then use Review policy activation on the Controls page to inspect each Policy’s planned or partial Controls, missing Components or sources, missing schedules, and unresolved Exceptions.", "Choose the approved Policies that should take effect, set the real effective date, and confirm the Step 3 cutover. You can activate with a documented gap or approved Exception, but Evidence Readiness still requires active and operating Policies."], types: ["control", "complementary-control", "document"], defaultOpen: true }
|
|
122
122
|
],
|
|
123
|
-
resourceTypes: ["control", "complementary-control"],
|
|
123
|
+
resourceTypes: ["control", "complementary-control", "document"],
|
|
124
124
|
commands: [
|
|
125
125
|
"filegrc guide control --json",
|
|
126
126
|
"filegrc list control --json",
|
|
127
127
|
"filegrc get CONTROL_ID --mutation",
|
|
128
128
|
"filegrc review-collection complementary-control --scaffold",
|
|
129
|
+
"filegrc activate-policies --scaffold",
|
|
129
130
|
"filegrc evidence-map --json",
|
|
130
131
|
"filegrc program-readiness --json"
|
|
131
132
|
]
|
|
@@ -180,7 +181,7 @@ export const PROGRAM_PATH = [
|
|
|
180
181
|
summary: "Start a guided checklist when a policy-triggering change occurs.",
|
|
181
182
|
instructions: "Trigger the matching workflow when an event occurs. filegrc adds every required action to the Work Queue with its owner and deadline.",
|
|
182
183
|
use: "Preview the full workflow before triggering it, then create the event and every linked task in one validated write.",
|
|
183
|
-
policyBasis: "Active event
|
|
184
|
+
policyBasis: "Active event Obligations translate policy-triggering changes into owned, deadline-bound Action Items. They remain dormant until their governing Policies are active and effective.",
|
|
184
185
|
commands: ["filegrc obligations --json", "filegrc trigger EVENT_TYPE (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) --subject RESOURCE_ID --json"]
|
|
185
186
|
},
|
|
186
187
|
{
|
|
@@ -189,7 +190,7 @@ export const PROGRAM_PATH = [
|
|
|
189
190
|
summary: "Complete scheduled, event-driven, and assigned work by its due date.",
|
|
190
191
|
instructions: "Complete recurring work, Policy Event tasks, and assigned Action Items within their allowed windows, link the requested dated proof, and resolve overdue items.",
|
|
191
192
|
use: "See proposed, upcoming, blocked, due, and overdue policy work together with every open Action Item. Continuous and per-transaction Controls still operate through their Components and need dated operating records or Evidence.",
|
|
192
|
-
policyBasis: "
|
|
193
|
+
policyBasis: "Active and effective Policies start enabled reusable Obligations. Policy Events and source records create owned Action Items. Each occurrence or task retains its own deadline, completion record, and evidence.",
|
|
193
194
|
commands: [
|
|
194
195
|
"filegrc obligations --json",
|
|
195
196
|
"filegrc complete OBLIGATION_ID --scaffold --window-start YYYY-MM-DD --completed-on YYYY-MM-DD",
|