filegrc 0.1.0 → 0.3.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/LICENSE +1 -1
- package/README.md +24 -5
- package/model/v1.json +163 -169
- package/package.json +2 -2
- package/src/agent.js +4 -0
- package/src/audit-preparation.js +217 -139
- package/src/cli.js +364 -9
- package/src/evidence-packet.js +78 -25
- package/src/evidence-tests.js +69 -0
- package/src/git.js +2 -1
- package/src/index.js +12 -1
- package/src/model-docs.js +51 -6
- package/src/obligations.js +87 -11
- package/src/parties.js +46 -0
- package/src/paths.js +1 -1
- package/src/program-lifecycle.js +25 -0
- package/src/program-path.js +275 -0
- package/src/program-readiness.js +652 -0
- package/src/resource-markdown.js +11 -4
- package/src/server.js +10 -2
- package/src/setup.js +187 -0
- package/src/state.js +9 -2
- package/src/validate.js +57 -24
- package/src/web.js +827 -440
package/src/obligations.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "./recurrence.js";
|
|
11
11
|
import { isRfc3339Timestamp } from "./time.js";
|
|
12
12
|
import { loadWorkspace } from "./workspace.js";
|
|
13
|
+
import { obligationProgramStatus } from "./program-lifecycle.js";
|
|
13
14
|
|
|
14
15
|
const COMPLETION_DATE_FIELDS = [
|
|
15
16
|
"completedOn",
|
|
@@ -55,6 +56,7 @@ export function planObligations(resources, options = {}) {
|
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
for (const obligation of obligations) {
|
|
59
|
+
const programStatus = obligationProgramStatus(obligation, byId, asOf);
|
|
58
60
|
if (obligation.recurrence?.mode === "event" && obligation.recurrence.eventType) {
|
|
59
61
|
const eventType = obligation.recurrence.eventType;
|
|
60
62
|
const group = triggerGroups.get(eventType) ?? {
|
|
@@ -62,8 +64,10 @@ export function planObligations(resources, options = {}) {
|
|
|
62
64
|
prompt: obligation.triggerPrompt || humanize(eventType),
|
|
63
65
|
policyIds: [],
|
|
64
66
|
obligationIds: [],
|
|
67
|
+
programStatus,
|
|
65
68
|
steps: []
|
|
66
69
|
};
|
|
70
|
+
if (programStatus === "proposed") group.programStatus = "proposed";
|
|
67
71
|
group.policyIds.push(...(obligation.policyIds || []));
|
|
68
72
|
group.obligationIds.push(obligation.id);
|
|
69
73
|
group.steps.push({
|
|
@@ -76,15 +80,20 @@ export function planObligations(resources, options = {}) {
|
|
|
76
80
|
scopeResourceIds: obligation.scopeResourceIds || [],
|
|
77
81
|
templateResourceId: obligation.templateResourceId || null,
|
|
78
82
|
completionResourceTypes: obligation.completionResourceTypes || [],
|
|
83
|
+
programStatus,
|
|
79
84
|
window: normalizedEventWindow(obligation.window)
|
|
80
85
|
});
|
|
81
86
|
triggerGroups.set(eventType, group);
|
|
82
87
|
continue;
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
const configuredAnchor = obligation.recurrence?.anchorDate || obligation.startsOn;
|
|
91
|
+
const activationDate = obligationActivationDate(obligation, byId);
|
|
85
92
|
const recurrence = {
|
|
86
93
|
...(obligation.recurrence || {}),
|
|
87
|
-
anchorDate:
|
|
94
|
+
anchorDate: configuredAnchor && activationDate
|
|
95
|
+
? [configuredAnchor, activationDate].sort().at(-1)
|
|
96
|
+
: configuredAnchor || activationDate
|
|
88
97
|
};
|
|
89
98
|
if (!validCalendarRecurrence(recurrence)) continue;
|
|
90
99
|
const from = requestedFrom || recurrence.anchorDate;
|
|
@@ -106,7 +115,8 @@ export function planObligations(resources, options = {}) {
|
|
|
106
115
|
&& completionFallsInWindow(record, window)
|
|
107
116
|
&& completionTypeMatches(record, obligation.completionResourceTypes)
|
|
108
117
|
));
|
|
109
|
-
const
|
|
118
|
+
const timingStatus = occurrenceStatus(window, asOf, completions.length > 0);
|
|
119
|
+
const status = timingStatus === "complete" || programStatus === "accepted" ? timingStatus : "proposed";
|
|
110
120
|
if (status === "complete" && !options.includeComplete) continue;
|
|
111
121
|
if (calendarItems.length >= MAX_PLANNED_ITEMS) {
|
|
112
122
|
throw new Error(`The obligation query must be narrowed with a later from date; it exceeds ${MAX_PLANNED_ITEMS.toLocaleString("en-US")} calendar occurrences.`);
|
|
@@ -123,6 +133,8 @@ export function planObligations(resources, options = {}) {
|
|
|
123
133
|
scopeResourceIds: obligation.scopeResourceIds || [],
|
|
124
134
|
completionResourceIds: completions.map((record) => record.id),
|
|
125
135
|
status,
|
|
136
|
+
timingStatus,
|
|
137
|
+
programStatus,
|
|
126
138
|
...window,
|
|
127
139
|
...relativeTiming(window, asOf)
|
|
128
140
|
});
|
|
@@ -149,11 +161,15 @@ export function planObligations(resources, options = {}) {
|
|
|
149
161
|
.filter((run) => run.status !== "canceled")
|
|
150
162
|
.flatMap((run) => run.actions)
|
|
151
163
|
.filter((item) => item.status !== "complete" || options.includeComplete);
|
|
152
|
-
|
|
164
|
+
const standaloneItems = records
|
|
165
|
+
.filter((record) => record.type === "action-item" && !eventIds.has(record.sourceResourceId))
|
|
166
|
+
.map((record) => planStandaloneAction(record, byId, asOf, now))
|
|
167
|
+
.filter((item) => item.status !== "complete" || options.includeComplete);
|
|
168
|
+
if (calendarItems.length + eventItems.length + standaloneItems.length > MAX_PLANNED_ITEMS) {
|
|
153
169
|
throw new Error(`The obligation query must be narrowed; it exceeds ${MAX_PLANNED_ITEMS.toLocaleString("en-US")} planned items.`);
|
|
154
170
|
}
|
|
155
|
-
const items = [...calendarItems, ...eventItems].sort(comparePlannedItems);
|
|
156
|
-
const counts = { overdue: 0, due: 0, upcoming: 0, complete: 0 };
|
|
171
|
+
const items = [...calendarItems, ...eventItems, ...standaloneItems].sort(comparePlannedItems);
|
|
172
|
+
const counts = { overdue: 0, due: 0, upcoming: 0, proposed: 0, complete: 0 };
|
|
157
173
|
for (const item of items) {
|
|
158
174
|
if (counts[item.status] !== undefined) counts[item.status] += 1;
|
|
159
175
|
}
|
|
@@ -166,6 +182,7 @@ export function planObligations(resources, options = {}) {
|
|
|
166
182
|
items,
|
|
167
183
|
calendarItems,
|
|
168
184
|
eventItems,
|
|
185
|
+
standaloneItems,
|
|
169
186
|
triggers: [...triggerGroups.values()].map((group) => ({
|
|
170
187
|
...group,
|
|
171
188
|
policyIds: [...new Set(group.policyIds)],
|
|
@@ -178,6 +195,7 @@ export function planObligations(resources, options = {}) {
|
|
|
178
195
|
export async function createObligationEvent(input, options) {
|
|
179
196
|
const loaded = await loadWorkspace(input);
|
|
180
197
|
const records = loaded.resources;
|
|
198
|
+
const byId = new Map(records.map((record) => [record.id, record]));
|
|
181
199
|
const eventType = String(options?.eventType || "").trim();
|
|
182
200
|
const occurredAt = options?.occurredAt ? requireTimestamp(options.occurredAt, "event timestamp") : null;
|
|
183
201
|
const timestampDate = timestampCalendarDate(occurredAt, loaded.workspace.timezone);
|
|
@@ -195,6 +213,9 @@ export async function createObligationEvent(input, options) {
|
|
|
195
213
|
&& record.recurrence.eventType === eventType
|
|
196
214
|
));
|
|
197
215
|
if (!eventType || templates.length === 0) throw new Error(`No active obligations use event type "${eventType}".`);
|
|
216
|
+
if (templates.some((record) => obligationProgramStatus(record, byId, occurredOn) === "proposed")) {
|
|
217
|
+
throw new Error(`Event type "${eventType}" still has starter proposals. Make every governing policy effective and implement at least one linked control before starting this workflow.`);
|
|
218
|
+
}
|
|
198
219
|
if (templates.some((record) => Number.isInteger(normalizedEventWindow(record.window).endOffsetHours)) && !occurredAt) {
|
|
199
220
|
throw new Error(`Event type "${eventType}" has hour-based deadlines and requires an RFC 3339 occurredAt timestamp.`);
|
|
200
221
|
}
|
|
@@ -236,7 +257,6 @@ export async function createObligationEvent(input, options) {
|
|
|
236
257
|
...(occurredAt ? { occurredAt } : {}),
|
|
237
258
|
ownerIds: [...new Set(templates.flatMap((record) => record.ownerIds || []))],
|
|
238
259
|
obligationIds: templates.map((record) => record.id),
|
|
239
|
-
actionItemIds: actions.map((record) => record.id),
|
|
240
260
|
...(subjectResourceIds.length ? { subjectResourceIds } : {})
|
|
241
261
|
};
|
|
242
262
|
await createResources(loaded.root, [event, ...actions]);
|
|
@@ -292,7 +312,7 @@ export async function completeObligationEvent(input, options) {
|
|
|
292
312
|
const event = loaded.resources.find((record) => (
|
|
293
313
|
record.type === "obligation-event" && record.id === options?.eventId
|
|
294
314
|
));
|
|
295
|
-
if (!event) throw new Error(`
|
|
315
|
+
if (!event) throw new Error(`Policy Event "${options?.eventId ?? ""}" was not found.`);
|
|
296
316
|
const completedOn = requireDate(options?.completedOn, "completion date");
|
|
297
317
|
if (completedOn < event.occurredOn) {
|
|
298
318
|
throw new Error("The event completion date cannot be before its occurrence date.");
|
|
@@ -304,12 +324,12 @@ export async function completeObligationEvent(input, options) {
|
|
|
304
324
|
});
|
|
305
325
|
const run = plan.eventRuns.find((item) => item.id === event.id);
|
|
306
326
|
if (!run || run.actions.length === 0) {
|
|
307
|
-
throw new Error(`
|
|
327
|
+
throw new Error(`Policy Event "${event.id}" has no action checklist.`);
|
|
308
328
|
}
|
|
309
329
|
const incomplete = run.actions.filter((action) => action.status !== "complete");
|
|
310
330
|
if (incomplete.length) {
|
|
311
331
|
throw new Error(
|
|
312
|
-
`
|
|
332
|
+
`Policy Event "${event.id}" still has incomplete actions: ${incomplete.map((action) => action.actionItemId).join(", ")}.`
|
|
313
333
|
);
|
|
314
334
|
}
|
|
315
335
|
return updateResource(loaded.root, "obligation-event", event.id, {
|
|
@@ -468,7 +488,7 @@ function planEventRun(event, actionItems, byId, asOf, now) {
|
|
|
468
488
|
occurredOn: event.occurredOn,
|
|
469
489
|
occurredAt: event.occurredAt || null,
|
|
470
490
|
subjectResourceIds: event.subjectResourceIds || [],
|
|
471
|
-
actionItemIds:
|
|
491
|
+
actionItemIds: actions.map((item) => item.actionItemId),
|
|
472
492
|
recordedStatus: event.status,
|
|
473
493
|
status: derivedStatus,
|
|
474
494
|
completeCount: actions.filter((item) => item.status === "complete").length,
|
|
@@ -476,6 +496,53 @@ function planEventRun(event, actionItems, byId, asOf, now) {
|
|
|
476
496
|
};
|
|
477
497
|
}
|
|
478
498
|
|
|
499
|
+
function planStandaloneAction(record, byId, asOf, now) {
|
|
500
|
+
const source = byId.get(record.sourceResourceId);
|
|
501
|
+
const dueWindowEnd = record.dueWindowEnd || record.dueOn || null;
|
|
502
|
+
const dueWindowStart = record.dueWindowStart || dueWindowEnd;
|
|
503
|
+
const dueWindowEndAt = record.dueWindowEndAt || null;
|
|
504
|
+
const dueWindowStartAt = record.dueWindowStartAt || dueWindowEndAt;
|
|
505
|
+
const window = {
|
|
506
|
+
dueWindowStart,
|
|
507
|
+
dueWindowEnd,
|
|
508
|
+
overdueOn: record.overdueOn || (dueWindowEnd ? addCalendarDays(dueWindowEnd, 1) : null),
|
|
509
|
+
dueWindowStartAt,
|
|
510
|
+
dueWindowEndAt,
|
|
511
|
+
overdueAt: record.overdueAt || dueWindowEndAt
|
|
512
|
+
};
|
|
513
|
+
const complete = ["done", "canceled"].includes(record.status);
|
|
514
|
+
const status = complete
|
|
515
|
+
? "complete"
|
|
516
|
+
: window.overdueAt && new Date(now) > new Date(window.overdueAt)
|
|
517
|
+
? "overdue"
|
|
518
|
+
: window.dueWindowStartAt && new Date(now) < new Date(window.dueWindowStartAt)
|
|
519
|
+
? "upcoming"
|
|
520
|
+
: !window.overdueAt && window.overdueOn && window.overdueOn <= asOf
|
|
521
|
+
? "overdue"
|
|
522
|
+
: window.dueWindowStart && window.dueWindowStart > asOf
|
|
523
|
+
? "upcoming"
|
|
524
|
+
: "due";
|
|
525
|
+
return {
|
|
526
|
+
key: record.id,
|
|
527
|
+
kind: "action",
|
|
528
|
+
actionItemId: record.id,
|
|
529
|
+
sourceResourceId: record.sourceResourceId,
|
|
530
|
+
title: record.title,
|
|
531
|
+
ownerIds: record.assigneeIds || [],
|
|
532
|
+
policyIds: source?.policyIds || [],
|
|
533
|
+
controlIds: source?.controlIds || [],
|
|
534
|
+
systemIds: source?.systemIds || [],
|
|
535
|
+
completionResourceIds: record.completionResourceIds || [],
|
|
536
|
+
evidenceIds: record.evidenceIds || [],
|
|
537
|
+
recordedStatus: record.status,
|
|
538
|
+
completedOn: record.completedOn || null,
|
|
539
|
+
status,
|
|
540
|
+
...window,
|
|
541
|
+
...relativeTiming(window, asOf),
|
|
542
|
+
...relativeTimestampTiming(window, now)
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
479
546
|
function completionFallsInWindow(record, window) {
|
|
480
547
|
const date = completionDate(record);
|
|
481
548
|
return Boolean(date && date >= window.dueWindowStart && date <= window.dueWindowEnd);
|
|
@@ -517,6 +584,15 @@ function occurrenceStatus(window, asOf, complete) {
|
|
|
517
584
|
return "upcoming";
|
|
518
585
|
}
|
|
519
586
|
|
|
587
|
+
function obligationActivationDate(obligation, byId) {
|
|
588
|
+
const dates = (obligation.policyIds || [])
|
|
589
|
+
.map((id) => byId.get(id))
|
|
590
|
+
.filter((policy) => policy?.type === "policy")
|
|
591
|
+
.map((policy) => policy.effectiveOn)
|
|
592
|
+
.filter(Boolean);
|
|
593
|
+
return dates.sort().at(-1) || null;
|
|
594
|
+
}
|
|
595
|
+
|
|
520
596
|
function relativeTiming(window, asOf) {
|
|
521
597
|
return {
|
|
522
598
|
daysUntilStart: window.dueWindowStart > asOf ? calendarDayDifference(asOf, window.dueWindowStart) : 0,
|
|
@@ -542,7 +618,7 @@ function relativeTimestampTiming(window, now) {
|
|
|
542
618
|
}
|
|
543
619
|
|
|
544
620
|
function comparePlannedItems(a, b) {
|
|
545
|
-
const rank = { overdue: 0, due: 1, upcoming: 2,
|
|
621
|
+
const rank = { overdue: 0, due: 1, upcoming: 2, proposed: 3, complete: 4 };
|
|
546
622
|
return (rank[a.status] - rank[b.status])
|
|
547
623
|
|| String(a.overdueAt || a.overdueOn || a.dueWindowEndAt || a.dueWindowEnd || a.dueWindowStartAt || a.dueWindowStart)
|
|
548
624
|
.localeCompare(String(b.overdueAt || b.overdueOn || b.dueWindowEndAt || b.dueWindowEnd || b.dueWindowStartAt || b.dueWindowStart))
|
package/src/parties.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const CURRENT_PERSON_STATUSES = new Set(["active", "external"]);
|
|
2
|
+
const CURRENT_TEAM_STATUSES = new Set(["active"]);
|
|
3
|
+
|
|
4
|
+
export function partyPeople(ids = [], byId, options = {}, seen = new Set()) {
|
|
5
|
+
const people = new Set();
|
|
6
|
+
for (const id of ids) {
|
|
7
|
+
if (seen.has(id)) continue;
|
|
8
|
+
seen.add(id);
|
|
9
|
+
const record = byId.get(id);
|
|
10
|
+
if (
|
|
11
|
+
record?.type === "person"
|
|
12
|
+
&& (!options.personStatuses || options.personStatuses.has(record.status))
|
|
13
|
+
) {
|
|
14
|
+
people.add(id);
|
|
15
|
+
}
|
|
16
|
+
if (
|
|
17
|
+
record?.type === "team"
|
|
18
|
+
&& (!options.teamStatuses || options.teamStatuses.has(record.status))
|
|
19
|
+
) {
|
|
20
|
+
for (const personId of partyPeople(
|
|
21
|
+
[...(record.memberIds || []), ...(record.chairIds || [])],
|
|
22
|
+
byId,
|
|
23
|
+
options,
|
|
24
|
+
seen
|
|
25
|
+
)) {
|
|
26
|
+
people.add(personId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return people;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function currentPartyPeople(ids = [], byId) {
|
|
34
|
+
return partyPeople(ids, byId, {
|
|
35
|
+
personStatuses: CURRENT_PERSON_STATUSES,
|
|
36
|
+
teamStatuses: CURRENT_TEAM_STATUSES
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function partiesIndependent(ownerIds = [], approverIds = [], byId) {
|
|
41
|
+
const owners = partyPeople(ownerIds, byId);
|
|
42
|
+
const approvers = partyPeople(approverIds, byId);
|
|
43
|
+
return owners.size > 0
|
|
44
|
+
&& approvers.size > 0
|
|
45
|
+
&& ![...owners].some((id) => approvers.has(id));
|
|
46
|
+
}
|
package/src/paths.js
CHANGED
|
@@ -15,7 +15,7 @@ export function resolveWorkspaceRoot(input = process.cwd()) {
|
|
|
15
15
|
current = parent;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
throw new Error("No
|
|
18
|
+
throw new Error("No filegrc workspace was found from the requested path.");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function isWithin(parent, candidate) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { currentPartyPeople } from "./parties.js";
|
|
2
|
+
|
|
3
|
+
export function obligationProgramStatus(obligation, byId, asOf) {
|
|
4
|
+
if (currentPartyPeople(obligation.ownerIds || [], byId).size === 0) return "proposed";
|
|
5
|
+
const policyIds = obligation.policyIds || [];
|
|
6
|
+
const policiesReady = policyIds.every((id) => {
|
|
7
|
+
const policy = byId.get(id);
|
|
8
|
+
return policy?.type === "policy"
|
|
9
|
+
&& policy.status === "active"
|
|
10
|
+
&& policy.effectiveOn
|
|
11
|
+
&& policy.effectiveOn <= asOf;
|
|
12
|
+
});
|
|
13
|
+
if (!policiesReady) return "proposed";
|
|
14
|
+
const controlIds = obligation.controlIds || [];
|
|
15
|
+
if (!controlIds.length) return "accepted";
|
|
16
|
+
return controlIds.some((id) => byId.get(id)?.type === "control" && byId.get(id).status === "implemented")
|
|
17
|
+
? "accepted"
|
|
18
|
+
: "proposed";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function obligationIsRunning(obligation, byId, asOf) {
|
|
22
|
+
return obligation?.type === "obligation"
|
|
23
|
+
&& obligation.status === "active"
|
|
24
|
+
&& obligationProgramStatus(obligation, byId, asOf) === "accepted";
|
|
25
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
export const RESOURCE_INSTRUCTIONS = {
|
|
2
|
+
person: "Confirm the policy owner created during setup, then add the actual people who will approve, review, or operate the program.",
|
|
3
|
+
team: "Review the starter Security and Risk Oversight team, including its members and chair. Add another team only when the organization assigns shared responsibility to it.",
|
|
4
|
+
system: "Catalog all in-scope systems for the program. Treat anything that operates a control or produces evidence as a System, including software provided by a vendor (like HR software).",
|
|
5
|
+
vendor: "Catalog the companies that provide in-scope software or services. Link each vendor-provided System to the company that provides it.",
|
|
6
|
+
framework: "Confirm the criteria framework and version used for the program.",
|
|
7
|
+
requirement: "Review each criterion, decide whether it applies, and record the reason for that decision.",
|
|
8
|
+
commitment: "Record supplemental customer promises and service requirements that shape the scope or control design.",
|
|
9
|
+
policy: "Tailor each policy to match how the organization works. Clear placeholders, assign an owner and separate approver, then record its approval and effective dates.",
|
|
10
|
+
document: "Tailor the governed plans and other supporting documents the program needs. Assign owners and approvers, then keep the approved Markdown in Git.",
|
|
11
|
+
control: "Finish each applicable starter control with the procedure people will follow, its owner, scope, cadence, evidence source, and implementation date.",
|
|
12
|
+
"complementary-control": "Record anything customers or carved-out providers must do for your controls to work as intended.",
|
|
13
|
+
evidence: "Complete the generated tests for external evidence that has no dedicated Step 5 record. Link each result to its Control and source System, then have another person verify it. When a Step 5 operating record exists, link the artifact’s External Evidence record there instead.",
|
|
14
|
+
"risk-assessment": "Complete and approve an assessment of the risks to the in-scope service, systems, vendors, and commitments.",
|
|
15
|
+
risk: "Record each risk identified by an assessment or operating activity. Assign an owner, rate it, and document the chosen response.",
|
|
16
|
+
obligation: "Review the recurring work proposed by effective policies. Confirm who owns it, when it is due, and what proof completion requires.",
|
|
17
|
+
"obligation-event": "When a policy-triggering event occurs, record it here and complete the actions filegrc creates for it.",
|
|
18
|
+
"policy-review": "Record scheduled and change-driven reviews of policies and governed documents, including the decision and any follow-up.",
|
|
19
|
+
meeting: "Record required oversight meetings, including attendees, decisions, minutes, and follow-up work.",
|
|
20
|
+
exception: "Record and approve any time-limited departure from a policy or control before the departure begins.",
|
|
21
|
+
asset: "Keep the inventory of important devices, software, media, and records current, including ownership, custody, and status.",
|
|
22
|
+
"vendor-review": "Document due diligence before relying on a provider, then repeat the review on schedule or after a material change.",
|
|
23
|
+
"access-grant": "Record each person’s or service account’s access to a System, including approval, provisioning, changes, and removal.",
|
|
24
|
+
"access-review": "Review access on schedule, record each decision, and assign any access changes that result.",
|
|
25
|
+
"service-account": "Catalog non-human accounts that need separate tracking, including their owner, purpose, System, privilege, and expiry.",
|
|
26
|
+
training: "Maintain the training content people must complete, along with its audience, timing, and passing requirements.",
|
|
27
|
+
attestation: "Record each person’s completion or acknowledgement against the exact policy or training revision.",
|
|
28
|
+
"vulnerability-scan": "Record each required scan, including its scope, timing, result, and evidence.",
|
|
29
|
+
vulnerability: "Track confirmed weaknesses that need separate remediation, acceptance, or closure.",
|
|
30
|
+
"penetration-test": "Record each penetration test, including its provider, scope, period, result, and evidence.",
|
|
31
|
+
incident: "Record qualifying security or privacy events and manage their response and follow-up.",
|
|
32
|
+
"backup-test": "Record each restore test, including the Systems tested, result, timing, evidence, and follow-up.",
|
|
33
|
+
exercise: "Record each incident or continuity exercise, including its objective, participants, result, and follow-up.",
|
|
34
|
+
finding: "Create a Finding only for a confirmed gap that needs separate remediation tracking. Keep the report details in the source record’s Markdown, then assign the Finding, set its due date, and verify closure.",
|
|
35
|
+
"action-item": "Create an Action Item only when follow-up needs its own assignee, deadline, and completion proof. Point it to the record that created the work, then work it from Work Queue.",
|
|
36
|
+
audit: "Create this record after engaging the CPA firm, then record the agreed scope, criteria, Systems, and report period.",
|
|
37
|
+
"audit-request": "Record each request from the audit team, assign an owner and due date, and link the approved response and evidence.",
|
|
38
|
+
"data-request": "Record privacy or contractual requests when they apply to the audit scope or the organization’s commitments.",
|
|
39
|
+
"control-test": "Record how an in-scope control was tested, what was sampled, the result, and any exceptions.",
|
|
40
|
+
"audit-population": "Record each complete Type 2 population with its source System, fixed export, query, count, and reconciliation."
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const POLICY_EVENT_NAMES = {
|
|
44
|
+
"person-started": "New Worker",
|
|
45
|
+
"person-ended": "Worker Departure",
|
|
46
|
+
"high-risk-person-ended": "High-Risk Departure",
|
|
47
|
+
"person-role-changed": "Role Change",
|
|
48
|
+
"personal-device-access-planned": "Personal Device Access",
|
|
49
|
+
"vendor-access-planned": "Vendor Access",
|
|
50
|
+
"vendor-reassessment-needed": "Vendor Reassessment",
|
|
51
|
+
"system-material-change": "Material System Change",
|
|
52
|
+
"material-incident": "Material Incident"
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function policyEventName(eventType) {
|
|
56
|
+
return POLICY_EVENT_NAMES[eventType] || humanize(eventType);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const PROGRAM_PATH = [
|
|
60
|
+
{
|
|
61
|
+
id: "scope",
|
|
62
|
+
number: 1,
|
|
63
|
+
title: "Define Scope",
|
|
64
|
+
description: "Ownership, criteria, and service boundary",
|
|
65
|
+
summary: "Confirm the people and teams responsible for the program, set the management goal, review the criteria and customer commitments in scope, then define the customer-facing service, supporting systems, and supplier dependencies.",
|
|
66
|
+
sections: [
|
|
67
|
+
{ id: "ownership", title: "Program Ownership", description: "Confirm the people and teams that own, approve, review, and operate the program.", steps: ["Confirm the policy owner created during setup, then add the organization’s actual 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", "team"], defaultOpen: true },
|
|
68
|
+
{ id: "criteria", title: "Criteria", description: "Confirm the criteria used for the program, resolve whether each requirement applies, and record customer commitments that shape the service or control design.", steps: ["Review the included Security criteria references.", "Mark each requirement applicable or not applicable with a rationale.", "Record customer commitments and keep optional criteria out until management deliberately adds them."], types: ["framework", "requirement", "commitment"], defaultOpen: true },
|
|
69
|
+
{ id: "boundary", title: "Service Boundary", description: "Record the service and its supporting technology and providers. An application or platform is a System because it operates controls or produces evidence; the company providing it is a Vendor because contracts, due diligence, and supplier risk belong to that relationship.", steps: ["Create a Vendor record for each material provider.", "Create System records for the customer-facing service and each supporting application, platform, or internal system that is in scope or produces evidence, then connect vendor-provided Systems to their providers.", "Assign owners, classification, dependencies, and a clear in-scope decision to each System, and keep supplier reviews with the Vendor."], types: ["vendor", "system"], defaultOpen: false }
|
|
70
|
+
],
|
|
71
|
+
resourceTypes: ["person", "team", "framework", "requirement", "commitment", "vendor", "system"],
|
|
72
|
+
commands: [
|
|
73
|
+
"filegrc setup",
|
|
74
|
+
"filegrc guide person --json",
|
|
75
|
+
"filegrc guide system --json",
|
|
76
|
+
"filegrc list system --json"
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "policies",
|
|
81
|
+
number: 2,
|
|
82
|
+
title: "Approve Policies",
|
|
83
|
+
description: "Tailor, review, approve, and adopt",
|
|
84
|
+
summary: "Turn every applicable policy and governed plan into the organization’s actual rules, remove placeholders, link governed controls, and establish approval and effective dates before scheduled work begins. The reviewer must be separate from the owner, is usually internal, and may be external.",
|
|
85
|
+
sections: [
|
|
86
|
+
{ id: "library", title: "Policy Library", description: "Review, approve, and activate policies and governed plans without treating starter text as adopted practice.", steps: ["Review policy Markdown and replace every organization placeholder.", "Confirm the owner, separate approver, audience, linked controls, and review cadence.", "Record approval and effective dates before changing the status to active."], types: ["policy", "document"], defaultOpen: true }
|
|
87
|
+
],
|
|
88
|
+
resourceTypes: ["policy", "document"],
|
|
89
|
+
commands: [
|
|
90
|
+
"filegrc guide policy --json",
|
|
91
|
+
"filegrc list policy --json",
|
|
92
|
+
"filegrc get POLICY_ID --mutation"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "controls",
|
|
97
|
+
number: 3,
|
|
98
|
+
title: "Implement Controls",
|
|
99
|
+
description: "Tailor and finish the starter control set",
|
|
100
|
+
summary: "Review the starter catalog against the scoped service, then give every applicable internal control an actual procedure, owner, system scope, cadence, policy and criteria mappings, authoritative evidence source, and implementation date. Mark it implemented only after the procedure is operating, then record any controls that customers or carved-out providers must perform.",
|
|
101
|
+
sections: [
|
|
102
|
+
{ id: "catalog", title: "Control Catalog", description: "Finish the starter controls, record applicable complementary controls, and see whether filegrc tracks operation through Work Queue or evidence records.", steps: ["Open every planned control and confirm its mappings and suggested frequency.", "Write the real procedure in Record Markdown and add system scope and evidence sources.", "Record any required customer or carved-out provider controls as Complementary Controls."], types: ["control", "complementary-control"], defaultOpen: true }
|
|
103
|
+
],
|
|
104
|
+
resourceTypes: ["control", "complementary-control"],
|
|
105
|
+
commands: [
|
|
106
|
+
"filegrc guide control --json",
|
|
107
|
+
"filegrc list control --json",
|
|
108
|
+
"filegrc get CONTROL_ID --mutation"
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "evidence",
|
|
113
|
+
number: 4,
|
|
114
|
+
title: "Test Evidence Collection",
|
|
115
|
+
description: "Verify sources before the period starts",
|
|
116
|
+
summary: "Before starting the candidate period, test external evidence collection only where no dedicated Step 5 operating record exists. When Step 5 already records the work, attach or reference the external artifact there instead of creating a separate test.",
|
|
117
|
+
sections: [
|
|
118
|
+
{ id: "collection", title: "Collection Test", description: "Test external evidence collection only where no dedicated Step 5 operating record exists.", steps: ["Confirm that each externally evidenced control names an authoritative source System.", "Complete each generated test export or capture.", "When a Step 5 operating record exists, link the artifact’s External Evidence record there instead."], relatedLinks: [{ label: "Source Systems", href: "#/resources/system" }, { label: "Controls", href: "#/resources/control" }], types: ["evidence"], defaultOpen: true }
|
|
119
|
+
],
|
|
120
|
+
resourceTypes: ["evidence"],
|
|
121
|
+
commands: [
|
|
122
|
+
"filegrc evidence-test-drafts --json",
|
|
123
|
+
"filegrc guide evidence --json",
|
|
124
|
+
"filegrc list evidence --json",
|
|
125
|
+
"filegrc program-readiness --json"
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "run",
|
|
130
|
+
number: 5,
|
|
131
|
+
title: "Operate the Program",
|
|
132
|
+
description: "Run the work and retain dated proof",
|
|
133
|
+
summary: "Record the management candidate start date when reliable evidence collection begins. Maintain current risk assessments and risks, updating the control set when needed. Complete recurring and event-driven work, run continuous and per-transaction controls, and keep dated evidence current throughout the period.",
|
|
134
|
+
sections: [
|
|
135
|
+
{ id: "risk", title: "Risk", description: "Maintain the program’s risk assessments and risk register as the service, threats, suppliers, and control needs change.", steps: ["Complete and approve risk assessments on schedule and after material changes.", "Record risks that need treatment, acceptance, or ongoing tracking.", "Add or update controls when the assessment identifies a new or changed response."], types: ["risk-assessment", "risk"], defaultOpen: true },
|
|
136
|
+
{ id: "queue", title: "Work Queue", description: "Complete recurring work, Policy Event tasks, and assigned follow-up within their required windows.", steps: ["Review proposed work while policies are drafts.", "Complete due work within its allowed window and link dated proof.", "Start Policy Events when hiring, departures, incidents, or material changes occur; every other open Action Item appears here automatically."], types: ["obligation", "obligation-event", "data-request"], utility: "obligation-board", defaultOpen: true },
|
|
137
|
+
{ id: "governance", title: "Governance", description: "Record formal reviews, oversight meetings, and approved policy or control exceptions.", steps: ["Complete scheduled policy reviews and oversight meetings.", "Record decisions, attendees, follow-up work, and evidence.", "Approve time-bound exceptions before the departure begins."], types: ["policy-review", "meeting", "exception"], defaultOpen: false },
|
|
138
|
+
{ id: "inventories", title: "Assets and Vendors", description: "Maintain the asset inventory and recurring reviews of supplier relationships during operation.", steps: ["Keep ownership, custody, status, and lifecycle current for important assets.", "Perform vendor reviews on schedule and after material supplier changes.", "Link fixed reports and review evidence to the operating records."], types: ["asset", "vendor-review"], defaultOpen: false },
|
|
139
|
+
{ id: "access-training", title: "Access and Training", description: "Inventory service accounts before recording access decisions, periodic reviews, assignments, and acknowledgements.", steps: ["Catalog service accounts that need separate tracking.", "Preserve access approvals and removals as they occur, then complete periodic access reviews and resolve exceptions.", "Assign training and retain acknowledgement evidence for the exact content revision."], types: ["service-account", "access-grant", "access-review", "training", "attestation"], defaultOpen: false },
|
|
140
|
+
{ id: "security", title: "Security Operations", description: "Record vulnerability work, independent testing, and incident response activity for the period.", steps: ["Retain scan scope, results, vulnerabilities, remediation, and exceptions.", "Track penetration testing and follow-up findings.", "Start the incident workflow when a qualifying event occurs."], types: ["vulnerability-scan", "vulnerability", "penetration-test", "incident"], defaultOpen: false },
|
|
141
|
+
{ id: "resilience", title: "Resilience", description: "Preserve proof that backups, restoration, continuity, and incident exercises work as designed.", steps: ["Record backup restoration tests and their results.", "Run continuity and incident exercises on schedule.", "Assign and close follow-up work from failed objectives or lessons learned."], types: ["backup-test", "exercise"], defaultOpen: false },
|
|
142
|
+
{ id: "issues", title: "Issues and Remediation", description: "Keep observations in the source report and track only confirmed gaps that need a separate remediation lifecycle.", steps: ["Create a Finding only when a confirmed gap needs its own owner, due date, status, or verified closure.", "Use the Finding itself for straightforward remediation; create Action Items only for separate assigned tasks.", "Work Action Items from Work Queue and close the Finding only after remediation is independently verified."], types: ["finding"], defaultOpen: false }
|
|
143
|
+
],
|
|
144
|
+
resourceTypes: [
|
|
145
|
+
"risk-assessment",
|
|
146
|
+
"risk",
|
|
147
|
+
"obligation",
|
|
148
|
+
"obligation-event",
|
|
149
|
+
"data-request",
|
|
150
|
+
"policy-review",
|
|
151
|
+
"meeting",
|
|
152
|
+
"exception",
|
|
153
|
+
"asset",
|
|
154
|
+
"vendor-review",
|
|
155
|
+
"service-account",
|
|
156
|
+
"access-grant",
|
|
157
|
+
"access-review",
|
|
158
|
+
"training",
|
|
159
|
+
"attestation",
|
|
160
|
+
"vulnerability-scan",
|
|
161
|
+
"vulnerability",
|
|
162
|
+
"penetration-test",
|
|
163
|
+
"incident",
|
|
164
|
+
"backup-test",
|
|
165
|
+
"exercise",
|
|
166
|
+
"finding"
|
|
167
|
+
],
|
|
168
|
+
supportingResourceTypes: ["action-item"],
|
|
169
|
+
utilities: [
|
|
170
|
+
{
|
|
171
|
+
id: "policy-events",
|
|
172
|
+
title: "Policy Events",
|
|
173
|
+
instructions: "Trigger the matching workflow when an event occurs. filegrc adds every required action to the Work Queue with its owner and deadline.",
|
|
174
|
+
use: "Preview the full workflow before triggering it, then create the event and every linked task in one validated write.",
|
|
175
|
+
policyBasis: "Active event obligations translate policy-triggering changes into owned, deadline-bound Action Items. Proposed workflows remain unavailable until their governing policies and linked controls are ready.",
|
|
176
|
+
commands: ["filegrc obligations --json", "filegrc trigger EVENT_TYPE (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) --subject RESOURCE_ID --json"]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: "work-queue",
|
|
180
|
+
title: "Work Queue",
|
|
181
|
+
instructions: "Complete recurring work, Policy Event tasks, and assigned Action Items within their allowed windows, link the requested dated proof, and resolve overdue items.",
|
|
182
|
+
use: "See proposed, upcoming, due, and overdue policy work together with every open Action Item. Continuous and per-transaction controls still operate in their source Systems and need dated operating records or evidence.",
|
|
183
|
+
policyBasis: "Effective policies and implemented linked controls activate reusable obligations. Policy Events and source records create owned Action Items. Each occurrence or task retains its own deadline, completion record, and evidence.",
|
|
184
|
+
commands: ["filegrc obligations --json", "filegrc complete OBLIGATION_ID completion-mutation.json --json"]
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
commands: [
|
|
188
|
+
"filegrc obligations --json",
|
|
189
|
+
"filegrc trigger EVENT_TYPE (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) --subject RESOURCE_ID --json",
|
|
190
|
+
"filegrc complete OBLIGATION_ID completion-mutation.json --json",
|
|
191
|
+
"filegrc complete-action ACTION_ITEM_ID completion-mutation.json --completed-on YYYY-MM-DD --json",
|
|
192
|
+
"filegrc complete-event OBLIGATION_EVENT_ID --completed-on YYYY-MM-DD --json",
|
|
193
|
+
"filegrc program-readiness --json"
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "audit",
|
|
198
|
+
number: 6,
|
|
199
|
+
title: "Audit",
|
|
200
|
+
description: "Firm, formal period, fieldwork, and report",
|
|
201
|
+
summary: "After the program is collecting reliable evidence, create an Audit record for the real CPA engagement, keep the firm-agreed report period separate from management’s candidate dates, complete management documents, populations, requests, evidence delivery, and fieldwork, then preserve the findings, responses, opinion, and final report.",
|
|
202
|
+
sections: [
|
|
203
|
+
{ id: "engagement", title: "Engagement", description: "Record the actual CPA engagement, formal scope and dates, requests, and management responses.", steps: ["Create the Audit after the CPA firm is engaged.", "Record the firm-agreed type, scope, systems, criteria, and dates.", "Track incoming requests and approved response material."], types: ["audit", "audit-request"], defaultOpen: true },
|
|
204
|
+
{ id: "fieldwork", title: "Fieldwork", description: "Prepare management documents, reconcile Type 2 populations, review both evidence paths, support testing, and build the indexed packet.", steps: ["Initialize engagement-specific management documents and populations.", "Review dated filegrc Evidence and verified External Evidence for the formal period.", "Reconcile complete populations, link samples, and resolve fieldwork requests and findings.", "Build the packet from a clean Git revision; it includes filegrc records, Markdown, External Evidence, attachments, indexes, history, and checksums."], types: ["audit-population", "control-test"], utility: "audit-packet", defaultOpen: true }
|
|
205
|
+
],
|
|
206
|
+
resourceTypes: ["audit", "audit-request", "audit-population", "control-test"],
|
|
207
|
+
utilities: [
|
|
208
|
+
{
|
|
209
|
+
id: "audit-packet",
|
|
210
|
+
title: "Audit Evidence & Packet",
|
|
211
|
+
instructions: "Review filegrc Evidence and External Evidence for the formal period, complete engagement preparation, and build the indexed audit packet.",
|
|
212
|
+
use: "Prepare management documents and populations, answer fieldwork requests, review both evidence paths, and compile a delivery bound to a clean Git revision.",
|
|
213
|
+
policyBasis: "Management prepares the scoped records, evidence, populations, assertions, and responses. The CPA firm selects samples, evaluates evidence and exceptions, and issues the report.",
|
|
214
|
+
commands: ["filegrc audit-readiness AUDIT_ID --json", "filegrc evidence-packet --audit AUDIT_ID --preview --json"]
|
|
215
|
+
}
|
|
216
|
+
],
|
|
217
|
+
commands: [
|
|
218
|
+
"filegrc guide audit --json",
|
|
219
|
+
"filegrc prepare-audit AUDIT_ID --json",
|
|
220
|
+
"filegrc audit-readiness AUDIT_ID --json",
|
|
221
|
+
"filegrc evidence-packet --audit AUDIT_ID --preview --json"
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
export function buildAgentProgramPath(model) {
|
|
227
|
+
return PROGRAM_PATH.map((stage) => {
|
|
228
|
+
const programResourceTypes = [...stage.resourceTypes, ...(stage.supportingResourceTypes || [])];
|
|
229
|
+
const resourcePages = programResourceTypes.map((type, index) => {
|
|
230
|
+
const definition = model.resources[type];
|
|
231
|
+
return {
|
|
232
|
+
order: stage.id === "run" ? null : `${stage.number}.${String.fromCharCode(97 + index)}`,
|
|
233
|
+
type,
|
|
234
|
+
title: definition.pluralTitle,
|
|
235
|
+
instructions: RESOURCE_INSTRUCTIONS[type] || definition.description,
|
|
236
|
+
use: definition.description,
|
|
237
|
+
policyBasis: definition.guidance.policyBasis,
|
|
238
|
+
guide: `filegrc guide ${type} --json`,
|
|
239
|
+
list: `filegrc list ${type} --json`
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
const utilityPages = (stage.utilities || []).map((utility, index) => ({
|
|
243
|
+
order: stage.id === "run" ? null : `${stage.number}.${String.fromCharCode(97 + stage.resourceTypes.length + index)}`,
|
|
244
|
+
utility: utility.id,
|
|
245
|
+
title: utility.title,
|
|
246
|
+
instructions: utility.instructions,
|
|
247
|
+
use: utility.use,
|
|
248
|
+
policyBasis: utility.policyBasis,
|
|
249
|
+
commands: utility.commands
|
|
250
|
+
}));
|
|
251
|
+
return {
|
|
252
|
+
...stage,
|
|
253
|
+
pages: stage.id === "run" ? utilityPages : [...resourcePages, ...utilityPages],
|
|
254
|
+
...(stage.id === "run" ? { operatingRecords: resourcePages } : {})
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function resourceProgramContext(type) {
|
|
260
|
+
const stage = PROGRAM_PATH.find((candidate) => (
|
|
261
|
+
candidate.resourceTypes.includes(type) || (candidate.supportingResourceTypes || []).includes(type)
|
|
262
|
+
));
|
|
263
|
+
if (!stage) return null;
|
|
264
|
+
const index = [...stage.resourceTypes, ...(stage.supportingResourceTypes || [])].indexOf(type);
|
|
265
|
+
return {
|
|
266
|
+
id: stage.id,
|
|
267
|
+
number: stage.number,
|
|
268
|
+
title: stage.title,
|
|
269
|
+
order: stage.id === "run" ? null : `${stage.number}.${String.fromCharCode(97 + index)}`
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function humanize(value) {
|
|
274
|
+
return String(value || "").replaceAll("-", " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
275
|
+
}
|