filegrc 0.11.0 → 0.12.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.
@@ -50,7 +50,7 @@ Management obtains or generates, checks, and uses relevant information from inte
50
50
 
51
51
  Everyone in scope must act honestly, protect company and customer information, follow approved security processes, disclose conflicts that could affect security decisions, and preserve accurate records. Fraud, deliberate Control bypass, false Evidence, credential sharing, unauthorized access, concealment of a security event, and retaliation for a good-faith report are prohibited.
52
52
 
53
- Suspected security events, Control failures, fraud, or policy violations must be reported promptly through the primary route at {{security_contact_email}} or the usable alternate route documented in the Security Incident and Recovery Plan. A person may use the alternate route when the primary route is unavailable, compromised, or involved in the concern. Management investigates credible reports, limits disclosure to people who need the information, preserves relevant records, and records corrective action.
53
+ Suspected security events, Control failures, fraud, or policy violations must be reported promptly through the current normal Security Reporting Channel or its usable fallback. A person may use the fallback when the normal channel is unavailable, compromised, or involved in the concern. Management investigates credible reports, limits disclosure to people who need the information, preserves relevant records, and records corrective action.
54
54
 
55
55
  ## Risk Management and Compliance Policy
56
56
 
@@ -15,11 +15,13 @@ const POLICY_DISPLAY_PATH = `data/${POLICY_CONTENT_PATH}`;
15
15
  const TRAINING_ID = "training-security-awareness";
16
16
  const TRAINING_CONTENT_PATH = "training/training-security-awareness.md";
17
17
  const TRAINING_DISPLAY_PATH = `data/${TRAINING_CONTENT_PATH}`;
18
+ const ALTERNATE_REPORTING_ROUTE_ID = "reporting-route-security-alternate";
18
19
  const PRIOR_TRAINING_REVISIONS = new Set([
19
20
  "53583484d668697e78d82bc22136d78976191303a06f67d0ad2edb55ba5ee0c6",
20
21
  "0c6677c22932eb3734240125bc0f40418a13f31d0a31943f4ecd12754afed7fc",
21
22
  "9e020af0de1c137607ba4c3186f04be7a00e0d0006e7fc8c7a109e35faf353cf",
22
- "bf8ac1a9e22a215ed375b590f3cba767801e2e062069e147290bad2c98fd29aa"
23
+ "bf8ac1a9e22a215ed375b590f3cba767801e2e062069e147290bad2c98fd29aa",
24
+ "7e55e8df6991d32085ca836c24e3b0015806d04523eca7ed4ee9e51043f05614"
23
25
  ]);
24
26
  const CURRENT_TRAINING_REVISION = "7e55e8df6991d32085ca836c24e3b0015806d04523eca7ed4ee9e51043f05614";
25
27
  const PRIOR_PASSWORD_MANAGER_TRAINING = "Use an approved password manager to generate and store a unique password for each account.";
@@ -37,6 +39,15 @@ const PRIOR_ENDPOINT_SETTING_TRAINING = "Company-managed devices use the automat
37
39
  const CURRENT_ENDPOINT_SETTING_TRAINING = "Company-managed devices must use the approved automatic-lock setting.";
38
40
  const PRIOR_CONFIGURATION_RECORD_TRAINING = "The applicable Controls, Components, Systems, and governed schedules record the actual tools, settings, approval paths, and evidence.";
39
41
  const CURRENT_CONFIGURATION_DOCUMENTATION_TRAINING = "Approved supporting standards, procedures, and schedules document the tools, settings, approval paths, and evidence.";
42
+ const PRIOR_TRAINING_REPORTING_ROUTE = "Questions and incident reports should be sent to {{security_contact_email}}.";
43
+ const PREVIOUS_TRAINING_REPORTING_ROUTE = "Questions and incident reports use the current approved security reporting route delivered with this assignment.";
44
+ const CURRENT_TRAINING_REPORTING_ROUTE = "Questions and incident reports use the current approved security reporting channel delivered with this assignment.";
45
+ const PRIOR_TRAINING_REPORTING_ACTION = "2. Report the event immediately to {{security_contact_email}}. If that route is unavailable, contact the current Policy Owner through an approved alternate method.";
46
+ const PREVIOUS_TRAINING_REPORTING_ACTION = "2. Report the event immediately through the approved security reporting route delivered with this assignment. If that route is unavailable, use the current approved alternate route.";
47
+ const CURRENT_TRAINING_REPORTING_ACTION = "2. Report the event immediately through the approved security reporting channel delivered with this assignment. If that channel is unavailable, use the current approved fallback channel.";
48
+ const PRIOR_TRAINING_REPORTING_CONFIRMATION = "- Confirm that you know how to reach {{security_contact_email}}.";
49
+ const PREVIOUS_TRAINING_REPORTING_CONFIRMATION = "- Confirm that you know how to use the security reporting route delivered with this assignment.";
50
+ const CURRENT_TRAINING_REPORTING_CONFIRMATION = "- Confirm that you know how to use the security reporting channel delivered with this assignment.";
40
51
  const DOCUMENT_CONTENT_UPDATES = [
41
52
  {
42
53
  id: "document-data-retention-schedule",
@@ -59,9 +70,18 @@ const DOCUMENT_CONTENT_UPDATES = [
59
70
  id: "document-security-incident-recovery-plan",
60
71
  path: "documents/document-security-incident-recovery-plan.md",
61
72
  priorRevision: "339ff564fa0ec26503c2498e8d3c44529957113cf782aa03ddb5b4e64c8f0404",
62
- additionalPriorRevisions: new Set(["51a19e22f3e0b31196371676297bfa843f96295d2f6c72e81a969ce7bafc36ae"]),
63
- currentRevision: "558b6fa71eaabf4ea3cfe77eaa24f827ad66a19a059085b2090e3c4a4d79cb50",
73
+ additionalPriorRevisions: new Set([
74
+ "51a19e22f3e0b31196371676297bfa843f96295d2f6c72e81a969ce7bafc36ae",
75
+ "5107f5c6015089a7c64e87692976aa48f727b8c03988f93f71e8f3e5084a05ac",
76
+ "970b97c4f594ff54fab554460f3cf44f80ee8079e996f4d5dc1427290f8aa714"
77
+ ]),
78
+ currentRevision: "ff8b5764ba3073d7ab6678c7da607850de5a82de4383cb54417cd74d82c05305",
64
79
  replacements: [
80
+ ["## Reporting routes", "## Reporting channels"],
81
+ ["The primary reporting route is {{security_contact_email}}.", "Use the current approved primary security Reporting Route. The Reporting Route record is the source of truth for its channel, destination, owner, priority, and effective period."],
82
+ ["Use the current approved primary security Reporting Route. The Reporting Route record is the source of truth for its channel, destination, owner, priority, and effective period.", "Use the current approved primary security Reporting Route. If it is unavailable, use the current approved alternate Reporting Route. Those records are the source of truth for each channel, destination, owner, priority, and effective period."],
83
+ ["[Complete before activation: Name a usable alternate reporting route, its owner, protected location, and how workers can find it when the primary email, identity, or collaboration System is unavailable, compromised, or involved in the concern. Do not put plaintext credentials, private keys, tokens, or recovery codes in this plan.]", "[Complete before activation: Describe how workers can find the alternate Reporting Route when the primary email, identity, or collaboration System is unavailable, compromised, or involved in the concern. Do not copy the route destination here or include plaintext credentials, private keys, tokens, or recovery codes.]"],
84
+ ["Use the current approved primary security Reporting Route. If it is unavailable, use the current approved alternate Reporting Route. Those records are the source of truth for each channel, destination, owner, priority, and effective period.\n\n[Complete before activation: Describe how workers can find the alternate Reporting Route when the primary email, identity, or collaboration System is unavailable, compromised, or involved in the concern. Do not copy the route destination here or include plaintext credentials, private keys, tokens, or recovery codes.]", "People send suspected security concerns through the current approved normal reporting channel, such as the security email address. If that channel is unavailable, compromised, or involved in the concern, they use the approved fallback channel. The Reporting Channel Set is the source of truth for the current destinations, responsible role, and effective period.\n\n[Complete before activation: Describe how workers can find the fallback reporting channel when the normal email, identity, or collaboration System is unavailable, compromised, or involved in the concern. Do not copy the destination here or include plaintext credentials, private keys, tokens, or recovery codes.]"],
65
85
  ["Controls, Components, Systems, Obligations, and Evidence hold the actual technical configuration and proof of operation.", "Supporting procedures, system records, and retained evidence document the actual technical configuration and operation."],
66
86
  ["If an incident raises a legal, privacy, or insurance question, the incident lead gets suitable advice at that time. FileGRC does not require pre-arranged counsel, in-house counsel, or a standing legal retainer.", "If an incident raises a legal, privacy, or insurance question, the incident lead obtains suitable advice at that time. A pre-arranged counsel relationship or standing legal retainer is required only when management determines that the organization's obligations and risk warrant one."],
67
87
  ["[Complete before activation: Link every important System and record its approved recovery time objective, recovery point objective, maximum tolerable downtime, dependencies, owner, and critical customer commitments in the System record.]", "[Complete before activation: Document every important System's approved recovery time objective, recovery point objective, maximum tolerable downtime, dependencies, owner, and critical customer commitments.]"],
@@ -565,7 +585,13 @@ async function buildPolicyLibraryPlan(loaded) {
565
585
  .replace(PRIOR_REPOSITORY_SECRET_TRAINING, CURRENT_REPOSITORY_SECRET_TRAINING)
566
586
  .replace(PRIOR_SOURCE_SECRET_TRAINING, CURRENT_SOURCE_SECRET_TRAINING)
567
587
  .replace(PRIOR_ENDPOINT_SETTING_TRAINING, CURRENT_ENDPOINT_SETTING_TRAINING)
568
- .replace(PRIOR_CONFIGURATION_RECORD_TRAINING, CURRENT_CONFIGURATION_DOCUMENTATION_TRAINING);
588
+ .replace(PRIOR_CONFIGURATION_RECORD_TRAINING, CURRENT_CONFIGURATION_DOCUMENTATION_TRAINING)
589
+ .replace(PREVIOUS_TRAINING_REPORTING_ROUTE, CURRENT_TRAINING_REPORTING_ROUTE)
590
+ .replace(PRIOR_TRAINING_REPORTING_ROUTE, CURRENT_TRAINING_REPORTING_ROUTE)
591
+ .replace(PREVIOUS_TRAINING_REPORTING_ACTION, CURRENT_TRAINING_REPORTING_ACTION)
592
+ .replace(PRIOR_TRAINING_REPORTING_ACTION, CURRENT_TRAINING_REPORTING_ACTION)
593
+ .replace(PREVIOUS_TRAINING_REPORTING_CONFIRMATION, CURRENT_TRAINING_REPORTING_CONFIRMATION)
594
+ .replace(PRIOR_TRAINING_REPORTING_CONFIRMATION, CURRENT_TRAINING_REPORTING_CONFIRMATION);
569
595
  proposalChanges.push({
570
596
  resourceType: "training",
571
597
  resourceId: TRAINING_ID,
@@ -600,12 +626,16 @@ async function buildPolicyLibraryPlan(loaded) {
600
626
  && !documentUpdate.additionalPriorRevisions?.has(sourceRevision)) {
601
627
  skipped.push(skippedItem(documentUpdate.id, "customized", "The governed Document differs from the recognized prior starter, so FileGRC will not rewrite it."));
602
628
  } else {
629
+ const securityContact = source.match(/The primary reporting route is ([^\r\n]+)\./)?.[1] || null;
603
630
  const nextSource = documentUpdate.currentSourcePath
604
631
  ? materializeDocument(
605
632
  await readFile(new URL(documentUpdate.currentSourcePath, import.meta.url), "utf8"),
606
633
  loaded.workspace?.organizationName
607
634
  )
608
- : documentUpdate.replacements.reduce((current, [prior, next]) => current.replace(prior, next), source);
635
+ : documentUpdate.replacements.reduce((current, [prior, next]) => current.replace(
636
+ securityContact ? prior.replaceAll("{{security_contact_email}}", securityContact) : prior,
637
+ securityContact ? next.replaceAll("{{security_contact_email}}", securityContact) : next
638
+ ), source);
609
639
  if (normalizedDocumentRevision(nextSource, loaded.workspace?.organizationName) !== documentUpdate.currentRevision) {
610
640
  throw new Error(`The ${documentUpdate.id} starter update does not produce the current governed Document.`);
611
641
  }
@@ -725,17 +755,23 @@ async function buildPolicyLibraryPlan(loaded) {
725
755
  skipped.push(skippedItem(addition.id, "missing-basis", "The recognized proposed starter Obligation needed to derive this schedule is not available."));
726
756
  continue;
727
757
  }
758
+ const sourceRuleId = source.activeRuleId || source.ruleIds?.[0];
759
+ const sourceRule = sourceRuleId ? byId.get(sourceRuleId)?.record : null;
760
+ const sourceSchedule = source.scheduleMode === "rule" && sourceRule?.type === "obligation-rule"
761
+ ? sourceRule
762
+ : source;
728
763
  const record = {
729
764
  id: addition.id,
730
765
  type: "obligation",
731
766
  title: addition.title,
732
767
  status: "proposed",
733
768
  activityType: addition.activityType,
734
- recurrence: structuredClone(source.recurrence),
769
+ ...(modelSupports(loaded.model, "rolled-up-obligations") ? { scheduleMode: "legacy" } : {}),
770
+ recurrence: structuredClone(sourceSchedule.recurrence),
735
771
  ownerIds: [...(source.ownerIds || [])],
736
- startsOn: source.startsOn,
772
+ startsOn: source.startsOn || sourceSchedule.effectiveAt?.slice(0, 10),
737
773
  ...(source.triggerPrompt ? { triggerPrompt: source.triggerPrompt } : {}),
738
- ...(addition.window || source.window ? { window: structuredClone(addition.window || source.window) } : {}),
774
+ ...(addition.window || sourceSchedule.window ? { window: structuredClone(addition.window || sourceSchedule.window) } : {}),
739
775
  controlIds: [...addition.controlIds],
740
776
  policyIds: [...(source.policyIds || [])]
741
777
  };
@@ -750,6 +786,36 @@ async function buildPolicyLibraryPlan(loaded) {
750
786
  });
751
787
  }
752
788
 
789
+ if (modelSupports(loaded.model, "reporting-routes") && !byId.has(ALTERNATE_REPORTING_ROUTE_ID)) {
790
+ const primaryRoute = loaded.resources.find((record) => (
791
+ record.type === "reporting-route"
792
+ && record.purpose === "security-reporting"
793
+ && record.priority === "primary"
794
+ ));
795
+ if (primaryRoute) {
796
+ const record = {
797
+ id: ALTERNATE_REPORTING_ROUTE_ID,
798
+ type: "reporting-route",
799
+ title: "Alternate security reporting route",
800
+ status: "planned",
801
+ purpose: "security-reporting",
802
+ priority: "alternate",
803
+ channelKind: "other",
804
+ route: "TBD: protected alternate channel",
805
+ ownerIds: [...(primaryRoute.ownerIds || [])]
806
+ };
807
+ creates.push(record);
808
+ const displayPath = `data/reporting-routes/${record.id}.json`;
809
+ proposalChanges.push({
810
+ resourceType: "reporting-route",
811
+ resourceId: record.id,
812
+ path: displayPath,
813
+ summary: "Add a planned alternate route as the single source of truth for its destination, owner, and effective period.",
814
+ diff: fullReplacementDiff(displayPath, "", `${JSON.stringify(record, null, 2)}\n`)
815
+ });
816
+ }
817
+ }
818
+
753
819
  const proposals = proposalChanges.length ? [{
754
820
  id: INFORMATION_SECURITY_LIBRARY_PROPOSAL_ID,
755
821
  revision: createHash("sha256").update(JSON.stringify(proposalChanges)).digest("hex"),
@@ -801,6 +867,10 @@ function normalizedPolicyRevision(source, organizationName) {
801
867
  /through the primary route at [^\r\n]+ or the usable alternate route documented/,
802
868
  "through the primary route at {{security_contact_email}} or the usable alternate route documented"
803
869
  );
870
+ normalized = normalized.replace(
871
+ "through the current primary Security Reporting Route or its current usable alternate route",
872
+ "through the primary route at {{security_contact_email}} or the usable alternate route documented in the Security Incident and Recovery Plan"
873
+ );
804
874
  return createHash("sha256").update(normalized).digest("hex");
805
875
  }
806
876
 
@@ -814,6 +884,13 @@ function normalizedTrainingRevision(source, organizationName) {
814
884
  }
815
885
  const securityContact = normalized.match(/Questions and incident reports should be sent to ([^\r\n]+)\./)?.[1];
816
886
  if (securityContact) normalized = normalized.replaceAll(securityContact, "{{security_contact_email}}");
887
+ normalized = normalized
888
+ .replace(CURRENT_TRAINING_REPORTING_ROUTE, PRIOR_TRAINING_REPORTING_ROUTE)
889
+ .replace(PREVIOUS_TRAINING_REPORTING_ROUTE, PRIOR_TRAINING_REPORTING_ROUTE)
890
+ .replace(CURRENT_TRAINING_REPORTING_ACTION, PRIOR_TRAINING_REPORTING_ACTION)
891
+ .replace(PREVIOUS_TRAINING_REPORTING_ACTION, PRIOR_TRAINING_REPORTING_ACTION)
892
+ .replace(CURRENT_TRAINING_REPORTING_CONFIRMATION, PRIOR_TRAINING_REPORTING_CONFIRMATION);
893
+ normalized = normalized.replace(PREVIOUS_TRAINING_REPORTING_CONFIRMATION, PRIOR_TRAINING_REPORTING_CONFIRMATION);
817
894
  return createHash("sha256").update(normalized).digest("hex");
818
895
  }
819
896
 
@@ -6,12 +6,13 @@ export const RESOURCE_INSTRUCTIONS = {
6
6
  system: "Start with the complete bounded System management governs or the auditor will examine. Record its purpose, services, boundary, exclusions, Information Types, owners, and any applicable continuity objectives.",
7
7
  component: "Add a Component only when it materially delivers a selected System, supports a Control, produces authoritative Evidence, or supports relevant operations. Give every System use a role and rationale.",
8
8
  vendor: "Catalog material external provider relationships. Link a supplied Component when it meets the Component inclusion rules, but do not mirror every Vendor into a Component.",
9
- classification: "Define an ordered information-handling category used by inventory and Evidence Artifacts.",
9
+ classification: "Review the starter information-handling categories and ordering against management's actual protection decisions, then use the approved Classifications on inventory and Evidence Artifacts.",
10
10
  "information-type": "Define a stable category of information and its default Classification, then link it from Systems, Components, and Vendors.",
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
14
  "requirement-mapping": "Record a reviewed relationship among Requirements, Commitments, and Controls. Choose the comparison method and relationship explicitly, explain the rationale, and bind the review to every mapped source revision.",
15
+ "reporting-route-set": "Record the normal and fallback ways people send one kind of report, including where each channel goes and the role that keeps it usable. Commit the proposal before approval, use separate approval and ongoing authority Appointments, and create a successor when either channel changes.",
15
16
  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.",
16
17
  document: "Complete required program Documents in Step 2, assign an owner and separate approver, and bind approval to the intended values and exact Markdown. Implement the linked requirements and activate that approved revision in Step 3. Prepare Audit Documents in Step 5.",
17
18
  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.",
@@ -56,6 +57,7 @@ export const RESOURCE_PAGE_SUMMARIES = {
56
57
  requirement: "Decide which SOC 2 criteria apply.",
57
58
  commitment: "Record customer promises that affect scope.",
58
59
  "requirement-mapping": "Review how supplemental promises relate to Requirements and Controls.",
60
+ "reporting-route-set": "Set the normal and fallback ways people report security concerns.",
59
61
  vendor: "List material external providers.",
60
62
  system: "Define the service boundary.",
61
63
  component: "Connect each material Component to a System.",
@@ -80,19 +82,21 @@ export const PROGRAM_PATH = [
80
82
  description: "Ownership, criteria, and service boundary",
81
83
  summary: "Name the owners, criteria, service, Systems, and providers in scope.",
82
84
  sections: [
83
- { 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 },
85
+ { id: "ownership", title: "Program Ownership", description: "Confirm who owns the program, plus the normal security reporting channel and its fallback.", 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.", "Set a normal security reporting channel and a fallback, then commit that proposal before approval.", "Add other teams only when the organization assigns shared responsibility to them."], types: ["person", "appointment", "team", "reporting-route-set"], defaultOpen: true },
84
86
  { 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 reviewed mappings. Keep optional criteria out until the company chooses to add them."], types: ["program", "framework", "requirement", "commitment", "requirement-mapping"], defaultOpen: true },
85
87
  { 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 }
86
88
  ],
87
- resourceTypes: ["person", "appointment", "team", "program", "framework", "requirement", "commitment", "requirement-mapping", "system", "component", "vendor", "classification", "information-type"],
89
+ resourceTypes: ["person", "appointment", "team", "reporting-route-set", "program", "framework", "requirement", "commitment", "requirement-mapping", "system", "component", "vendor", "classification", "information-type"],
88
90
  commands: [
89
91
  "filegrc setup",
90
92
  "filegrc guide person --json",
91
93
  "filegrc guide appointment --json",
94
+ "filegrc reporting-route-sets --json",
92
95
  "filegrc guide system --json",
93
96
  "filegrc guide component --json",
94
97
  "filegrc guide requirement-mapping --json",
95
98
  "filegrc review-collection vendor --scaffold",
99
+ "filegrc review-collection classification --scaffold",
96
100
  "filegrc review-collection information-type --scaffold",
97
101
  "filegrc list system --json"
98
102
  ]
@@ -201,11 +205,13 @@ export const PROGRAM_PATH = [
201
205
  summary: "Complete scheduled, event-driven, and assigned work by its due date.",
202
206
  instructions: "Complete recurring work, Policy Event tasks, and assigned Action Items within their allowed windows, link the requested dated proof, and resolve overdue items.",
203
207
  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.",
204
- 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.",
208
+ policyBasis: "Active and effective Policies start enabled reusable Obligations. Each calendar occurrence stays one rolled-up item whose reconciliation binds the selected population to its completion records. Policy Events and source records create owned Action Items.",
205
209
  commands: [
206
210
  "filegrc obligations --json",
211
+ "filegrc activate-obligation-rule RULE_ID --scaffold",
207
212
  "filegrc complete OBLIGATION_ID --scaffold --window-start YYYY-MM-DD --completed-on YYYY-MM-DD",
208
- "filegrc complete OBLIGATION_ID completion-mutation.json --json"
213
+ "filegrc complete OBLIGATION_ID completion-mutation.json --json",
214
+ "filegrc reconcile-obligation OBLIGATION_ID --scaffold --window-start YYYY-MM-DD"
209
215
  ]
210
216
  }
211
217
  ],
@@ -19,6 +19,7 @@ import { assessPolicyLibraryUpgrades } from "./policy-library.js";
19
19
  import { assessProgramAmendmentReadiness } from "./program-amendment.js";
20
20
  import { programComponents, resolveProgram, selectedRequirementIds } from "./program.js";
21
21
  import { assessRequirementMappingReadiness } from "./requirement-mapping.js";
22
+ import { assessReportingRouteSets } from "./reporting-route-sets.js";
22
23
  import { assessRetentionReadiness } from "./retention.js";
23
24
  import { markdownEntries } from "./resource-markdown.js";
24
25
  import {
@@ -28,7 +29,7 @@ import {
28
29
  REQUIRED_SOC2_SECURITY_REFERENCES
29
30
  } from "./soc2.js";
30
31
  import { assessSourceCoverageReadiness } from "./source-coverage.js";
31
- import { currentCalendarDate } from "./time.js";
32
+ import { currentCalendarDate, timestampFromLocalDateTime } from "./time.js";
32
33
  import { loadWorkspace } from "./workspace.js";
33
34
 
34
35
  export async function assessProgramReadiness(input, options = {}) {
@@ -60,7 +61,7 @@ export async function assessProgramReadiness(input, options = {}) {
60
61
  informationTypesReviewed: collectionReviews.find(({ resourceType }) => resourceType === "information-type")?.complete === true
61
62
  }));
62
63
  controlStage.items.push(...collectionReviews
63
- .filter(({ resourceType }) => ["component", "retention-schedule-item"].includes(resourceType))
64
+ .filter(({ resourceType }) => resourceType === "retention-schedule-item")
64
65
  .map(collectionReviewReadinessItem));
65
66
  const governedContent = await governedContentItems(scope, records, byId, readMarkdown, asOf, loaded.model);
66
67
  controlStage.items.push(...governedContent.items);
@@ -81,8 +82,16 @@ export async function assessProgramReadiness(input, options = {}) {
81
82
  records,
82
83
  byId,
83
84
  loaded.model,
84
- collectionReviews.filter(({ resourceType }) => ["person", "framework", "system", "vendor", "information-type"].includes(resourceType))
85
+ collectionReviews.filter(({ resourceType }) => ["person", "framework", "system", "component", "vendor", "classification", "information-type"].includes(resourceType)),
86
+ workspace?.timezone || "UTC"
85
87
  );
88
+ if (modelSupports(loaded.model, "reporting-route-sets")) {
89
+ const routeSets = await assessReportingRouteSets(loaded, {
90
+ programId: program.id,
91
+ at: timestampFromLocalDateTime(`${asOf}T23:59:59`, workspace?.timezone || "UTC")
92
+ });
93
+ scopeReadinessStage.items.push(reportingRouteSetItem(routeSets));
94
+ }
86
95
  scopeReadinessStage.items.push(...await assessRequirementMappingReadiness(loaded));
87
96
  scopeReadinessStage.items.push(...await assessProgramAmendmentReadiness(loaded));
88
97
  const evidenceGateStages = [
@@ -102,7 +111,7 @@ export async function assessProgramReadiness(input, options = {}) {
102
111
  && program.candidateCoverage?.kind === "range"
103
112
  && coverageStart(program.candidateCoverage) <= asOf
104
113
  );
105
- const obligations = planObligations(records, { asOf, through: asOf, model: loaded.model });
114
+ const obligations = planObligations(records, { programId: program.id, asOf, through: asOf, model: loaded.model });
106
115
  const policyLibrary = await assessPolicyLibraryUpgrades(loaded);
107
116
  const operating = evidenceReady && candidateStarted && stages.at(-1).counts.action === 0;
108
117
  const canStartCandidatePeriod = Boolean(
@@ -201,7 +210,7 @@ function programScope(program, records, byId, model, loaded) {
201
210
  };
202
211
  }
203
212
 
204
- function scopeStage(workspace, scope, records, byId, model, collectionReviews = []) {
213
+ function scopeStage(workspace, scope, records, byId, model, collectionReviews = [], timezone = "UTC") {
205
214
  const items = [];
206
215
  const goal = workspace?.assuranceGoal || "none";
207
216
  items.push(item(
@@ -220,6 +229,10 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
220
229
  }
221
230
  ));
222
231
 
232
+ if (model.resources?.["reporting-route"] && !modelSupports(model, "reporting-route-sets")) {
233
+ items.push(reportingRouteItem(records, byId, currentCalendarDate(timezone), timezone));
234
+ }
235
+
223
236
  if (modelSupports(model, "program-scope")) {
224
237
  const completeComponents = scope.components.filter((component) => (
225
238
  component.status === "active"
@@ -406,6 +419,71 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
406
419
  return stage("scope", "Define Scope", "Set program ownership, the management objective, service boundary, criteria, controls, and dependencies.", items);
407
420
  }
408
421
 
422
+ function reportingRouteSetItem(assessment) {
423
+ const current = assessment.routeSets.find(({ effective, canceled }) => effective && !canceled);
424
+ const draft = assessment.routeSets.find(({ record }) => ["draft", "proposed"].includes(record.status));
425
+ const required = assessment.requirements.length > 0;
426
+ const proposed = assessment.proposedRequirements.length > 0;
427
+ const ready = assessment.issues.length === 0 && (!required || Boolean(current));
428
+ const needsAction = assessment.issues.length > 0;
429
+ const target = current?.record || draft?.record || { type: "reporting-route-set" };
430
+ let message;
431
+ if (needsAction) {
432
+ message = assessment.issues[0].message;
433
+ } else if (required && ready && current) {
434
+ message = `${current.record.title} is committed, effective, and has a current responsible Appointment. Assignments bind its Git commit.`;
435
+ } else if (!required && proposed && draft) {
436
+ message = "A draft or approved-but-inactive rule will require normal and fallback reporting channels later. Review them before that rule becomes active; this is not a current readiness gate.";
437
+ } else if (!required && draft) {
438
+ message = "No approved rule currently requires these reporting channels. Keep the draft for review or remove it; it is not a readiness gate.";
439
+ } else if (!required) {
440
+ message = "No active Policy, Document, Commitment, or Risk decision currently requires these reporting channels.";
441
+ } else {
442
+ message = assessment.issues[0]?.message || "Set the normal and fallback reporting channels, commit the proposal, approve it, and commit the approval.";
443
+ }
444
+ return item(
445
+ "security-reporting-route-set",
446
+ needsAction ? "action" : required ? ready ? "complete" : "action" : proposed && draft ? "later" : "info",
447
+ needsAction ? "Fix reporting channel requirements" : required ? "Approve required reporting channels" : proposed ? "Review proposed reporting channels" : "Reporting channels are not currently required",
448
+ message,
449
+ target,
450
+ {
451
+ requirements: assessment.requirements,
452
+ issues: assessment.issues,
453
+ commands: [
454
+ "npx filegrc reporting-route-sets --json",
455
+ "npx filegrc reporting-route-set --scaffold"
456
+ ]
457
+ }
458
+ );
459
+ }
460
+
461
+ function reportingRouteItem(records, byId, asOf, timezone = "UTC") {
462
+ const cutoff = new Date(timestampFromLocalDateTime(`${asOf}T23:59:59`, timezone));
463
+ const routes = records.filter((record) => (
464
+ record.type === "reporting-route"
465
+ && record.purpose === "security-reporting"
466
+ && record.status === "active"
467
+ && new Date(record.effectiveAt) <= cutoff
468
+ && (!record.endsAt || new Date(record.endsAt) > cutoff)
469
+ ));
470
+ const routeByPriority = new Map(routes.map((route) => [route.priority, route]));
471
+ const required = ["primary", "alternate"];
472
+ const ready = required.every((priority) => {
473
+ const route = routeByPriority.get(priority);
474
+ return route && currentPartyPeople(route.ownerIds || [], byId).size > 0;
475
+ });
476
+ return item(
477
+ "security-reporting-route",
478
+ ready ? "complete" : "action",
479
+ "Activate the primary and alternate security reporting routes",
480
+ ready
481
+ ? `The current routes are ${required.map((priority) => routeByPriority.get(priority).title).join(" and ")}. Training assignments bind the exact Git revision delivered.`
482
+ : "Activate one owned primary route and one independently usable alternate route before assigning training or relying on policy reporting instructions.",
483
+ routes[0] || records.find(({ type }) => type === "reporting-route") || { type: "reporting-route" }
484
+ );
485
+ }
486
+
409
487
  function collectionReviewReadinessItem(assessment) {
410
488
  return item(
411
489
  `collection-review-${assessment.resourceType}`,
@@ -1377,7 +1455,7 @@ async function operationStage(loaded, workspace, scope, records, byId, asOf, evi
1377
1455
  ]);
1378
1456
  }
1379
1457
 
1380
- const obligations = planObligations(records, { asOf, through: asOf, model });
1458
+ const obligations = planObligations(records, { programId: workspace.id, asOf, through: asOf, model });
1381
1459
  const start = workspace.candidateCoverage?.kind === "range"
1382
1460
  ? coverageStart(workspace.candidateCoverage)
1383
1461
  : null;