filegrc 0.16.3 → 0.16.5
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/package.json +1 -1
- package/src/agent.js +2 -1
- package/src/audit-preparation.js +6 -1
- package/src/cli.js +71 -33
- package/src/index.js +1 -0
- package/src/program-path.js +141 -75
- package/src/program-readiness.js +194 -25
- package/src/reconciliation.js +1 -0
- package/src/retention-schedule-approval.js +5 -5
- package/src/state.js +1 -0
- package/src/web.js +72 -22
- package/src/workflow.js +1 -0
package/src/program-readiness.js
CHANGED
|
@@ -470,7 +470,7 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
470
470
|
)));
|
|
471
471
|
items.push(item(
|
|
472
472
|
"commitments",
|
|
473
|
-
scope.systems.length
|
|
473
|
+
!scope.systems.length ? "later" : uncoveredSystems.length === 0 && unscopedCommitments.length === 0 ? "complete" : "action",
|
|
474
474
|
"Record service commitments and system requirements",
|
|
475
475
|
scope.systems.length
|
|
476
476
|
? `${completeCommitments.length} complete active ${completeCommitments.length === 1 ? "commitment covers" : "commitments cover"} ${scope.systems.length - uncoveredSystems.length} of ${scope.systems.length} in-scope systems.${unscopedCommitments.length ? ` ${unscopedCommitments.length} Commitment${unscopedCommitments.length === 1 ? " has" : "s have"} no System scope and must be reviewed explicitly.` : ""}`
|
|
@@ -711,14 +711,23 @@ function reportingRouteItem(records, byId, asOf, timezone = "UTC") {
|
|
|
711
711
|
}
|
|
712
712
|
|
|
713
713
|
export function collectionReviewReadinessItem(assessment) {
|
|
714
|
+
const firstIncomplete = assessment.incompleteRecordProposals?.[0]
|
|
715
|
+
|| assessment.recordProposals?.find(({ complete }) => !complete);
|
|
716
|
+
const needsFirstRecord = assessment.recordCount === 0
|
|
717
|
+
&& !(assessment.configuration.decisions || []).some((decision) => ["zero-population", "externally-managed"].includes(decision));
|
|
718
|
+
const proposalAction = !assessment.complete && firstIncomplete;
|
|
719
|
+
const pendingRetentionRow = assessment.resourceType === "retention-schedule-item"
|
|
720
|
+
? assessment.records?.find(({ status }) => status === "planned")
|
|
721
|
+
: null;
|
|
722
|
+
const approvalAction = !proposalAction && !pendingRetentionRow
|
|
723
|
+
? collectionApprovalIssueAction(assessment)
|
|
724
|
+
: null;
|
|
714
725
|
const proposalUnits = (assessment.recordProposals || []).map((proposal) => ({
|
|
715
726
|
id: `${assessment.resourceType}-proposal-${proposal.resourceId}`,
|
|
716
727
|
status: proposal.complete ? "complete" : "action",
|
|
717
728
|
title: `${proposal.title} proposal`
|
|
718
729
|
}));
|
|
719
730
|
const proposalBatchComplete = proposalUnits.every(({ status }) => status === "complete");
|
|
720
|
-
const scheduleApprovalBlocked = assessment.resourceType === "retention-schedule-item"
|
|
721
|
-
&& proposalUnits.some(({ status }) => status !== "complete");
|
|
722
731
|
const batchProgressUnits = assessment.resourceType !== "retention-schedule-item" && proposalUnits.length
|
|
723
732
|
? [
|
|
724
733
|
{
|
|
@@ -737,24 +746,100 @@ export function collectionReviewReadinessItem(assessment) {
|
|
|
737
746
|
: null;
|
|
738
747
|
return item(
|
|
739
748
|
`collection-review-${assessment.resourceType}`,
|
|
740
|
-
assessment.complete ? "complete" :
|
|
741
|
-
|
|
742
|
-
? `
|
|
743
|
-
:
|
|
744
|
-
|
|
745
|
-
|
|
749
|
+
assessment.complete ? "complete" : pendingRetentionRow && !proposalAction ? "blocked" : "action",
|
|
750
|
+
proposalAction
|
|
751
|
+
? `Complete ${firstIncomplete.title} proposal`
|
|
752
|
+
: needsFirstRecord
|
|
753
|
+
? `Add a record for ${assessment.configuration.title.toLowerCase()}`
|
|
754
|
+
: approvalAction
|
|
755
|
+
? approvalAction.title
|
|
756
|
+
: assessment.status === "stale"
|
|
757
|
+
? `Review ${assessment.configuration.title.toLowerCase()} again`
|
|
758
|
+
: `Review ${assessment.configuration.title.toLowerCase()}`,
|
|
759
|
+
proposalAction
|
|
760
|
+
? `Complete ${firstIncomplete.title} before reviewing ${assessment.configuration.title.toLowerCase()}.`
|
|
761
|
+
: assessment.message,
|
|
762
|
+
proposalAction
|
|
763
|
+
? { id: firstIncomplete.resourceId, type: assessment.resourceType }
|
|
764
|
+
: needsFirstRecord ? { type: assessment.resourceType }
|
|
765
|
+
: approvalAction ? { type: approvalAction.resourceType, ...(approvalAction.resourceId ? { id: approvalAction.resourceId } : {}) }
|
|
766
|
+
: assessment.review || { type: assessment.resourceType },
|
|
746
767
|
{
|
|
747
|
-
resourceType: assessment.resourceType,
|
|
768
|
+
resourceType: approvalAction?.resourceType || assessment.resourceType,
|
|
748
769
|
reviewPoints: assessment.configuration.reviewPoints,
|
|
770
|
+
...(approvalAction?.createResource ? { createResource: true } : {}),
|
|
771
|
+
...(pendingRetentionRow && !proposalAction ? { unresolvedAssignments: [{
|
|
772
|
+
resourceType: "retention-schedule-item",
|
|
773
|
+
resourceId: pendingRetentionRow.id,
|
|
774
|
+
reasons: ["Complete or retire the planned retention row before approving the schedule."]
|
|
775
|
+
}] } : {}),
|
|
749
776
|
...(batchProgressUnits ? { progressUnits: batchProgressUnits } : {}),
|
|
750
|
-
commands:
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
777
|
+
commands: proposalAction
|
|
778
|
+
? [
|
|
779
|
+
`npx filegrc guide ${assessment.resourceType} --json`,
|
|
780
|
+
`npx filegrc get ${shellArgument(firstIncomplete.resourceId)} --mutation > MUTATION.json`,
|
|
781
|
+
`npx filegrc update ${assessment.resourceType} ${shellArgument(firstIncomplete.resourceId)} MUTATION.json --json`
|
|
782
|
+
]
|
|
783
|
+
: needsFirstRecord
|
|
784
|
+
? [
|
|
785
|
+
`npx filegrc guide ${assessment.resourceType} --json`,
|
|
786
|
+
`npx filegrc scaffold ${assessment.resourceType} --title "NAME" > MUTATION.json`,
|
|
787
|
+
"npx filegrc create MUTATION.json --json"
|
|
788
|
+
]
|
|
789
|
+
: approvalAction
|
|
790
|
+
? approvalAction.commands
|
|
791
|
+
: [
|
|
792
|
+
`npx filegrc review-collection ${assessment.resourceType} --scaffold > REVIEW.json`,
|
|
793
|
+
`npx filegrc review-collection ${assessment.resourceType} REVIEW.json --preview --json`,
|
|
794
|
+
`npx filegrc review-collection ${assessment.resourceType} REVIEW.json --yes --json`
|
|
795
|
+
]
|
|
754
796
|
}
|
|
755
797
|
);
|
|
756
798
|
}
|
|
757
799
|
|
|
800
|
+
function collectionApprovalIssueAction(assessment) {
|
|
801
|
+
const issue = assessment.approvalIssues?.[0];
|
|
802
|
+
if (!issue || issue.code === "invalid-retention-schedule-approval-binding") return null;
|
|
803
|
+
const rows = assessment.records || [];
|
|
804
|
+
const documentId = issue.resourceId || rows.find(({ scheduleDocumentId }) => scheduleDocumentId)?.scheduleDocumentId;
|
|
805
|
+
if (issue.code.includes("retention-schedule-document")) {
|
|
806
|
+
const resourceId = issue.code === "missing-retention-schedule-document" || issue.code === "multiple-retention-schedule-documents"
|
|
807
|
+
? null : documentId;
|
|
808
|
+
return {
|
|
809
|
+
title: issue.code === "missing-retention-schedule-document" ? "Add the Data Retention Schedule document"
|
|
810
|
+
: issue.code === "multiple-retention-schedule-documents" ? "Resolve duplicate Data Retention Schedule documents"
|
|
811
|
+
: "Complete the Data Retention Schedule document",
|
|
812
|
+
resourceType: "document",
|
|
813
|
+
resourceId,
|
|
814
|
+
createResource: issue.code === "missing-retention-schedule-document",
|
|
815
|
+
commands: resourceId
|
|
816
|
+
? ["npx filegrc guide document --json", `npx filegrc get ${shellArgument(resourceId)} --mutation > MUTATION.json`, `npx filegrc update document ${shellArgument(resourceId)} MUTATION.json --json`]
|
|
817
|
+
: issue.code === "missing-retention-schedule-document"
|
|
818
|
+
? ["npx filegrc guide document --json", 'npx filegrc scaffold document --title "Data Retention Schedule" > MUTATION.json', "npx filegrc create MUTATION.json --json"]
|
|
819
|
+
: ["npx filegrc guide document --json", "npx filegrc list document --workflow --json"]
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (["retention-row-wrong-schedule-document", "incomplete-retention-schedule-row"].includes(issue.code)) {
|
|
823
|
+
const row = rows.find(({ id }) => id === issue.resourceId);
|
|
824
|
+
if (row) return {
|
|
825
|
+
title: `Complete ${row.title}`,
|
|
826
|
+
resourceType: "retention-schedule-item",
|
|
827
|
+
resourceId: row.id,
|
|
828
|
+
commands: ["npx filegrc guide retention-schedule-item --json", `npx filegrc get ${shellArgument(row.id)} --mutation > MUTATION.json`, `npx filegrc update retention-schedule-item ${shellArgument(row.id)} MUTATION.json --json`]
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
if (issue.code === "unreviewed-similar-information-types") return {
|
|
832
|
+
title: "Review similar Information Types",
|
|
833
|
+
resourceType: "information-type",
|
|
834
|
+
commands: ["npx filegrc list information-type --workflow --json", "npx filegrc review-collection information-type --scaffold > REVIEW.json", "npx filegrc review-collection information-type REVIEW.json --preview --json", "npx filegrc review-collection information-type REVIEW.json --yes --json"]
|
|
835
|
+
};
|
|
836
|
+
return {
|
|
837
|
+
title: "Complete retention schedule coverage",
|
|
838
|
+
resourceType: "retention-schedule-item",
|
|
839
|
+
commands: ["npx filegrc guide retention-schedule-item --json", "npx filegrc list retention-schedule-item --workflow --json", 'npx filegrc scaffold retention-schedule-item --title "RETENTION RULE" > MUTATION.json', "npx filegrc create MUTATION.json --json"]
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
758
843
|
function requiredAppointmentsItem(records, model) {
|
|
759
844
|
const assessments = assessRequiredAppointments(records, model);
|
|
760
845
|
const incomplete = assessments.filter(({ requiredness, state }) => (
|
|
@@ -1508,10 +1593,12 @@ function legacyPolicyLibraryProposals(records) {
|
|
|
1508
1593
|
|
|
1509
1594
|
async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
1510
1595
|
const items = [];
|
|
1596
|
+
const families = selectedControlFamilies(scope.controls, model);
|
|
1511
1597
|
if (!scope.controls.length) {
|
|
1512
1598
|
items.push(item("control-scope", "action", "Select the program controls", "No controls are selected for the management program.", { type: "control" }));
|
|
1513
1599
|
}
|
|
1514
1600
|
for (const control of scope.controls) {
|
|
1601
|
+
const controlFamilies = families.filter((family) => family.controls.some(({ id }) => id === control.id));
|
|
1515
1602
|
const source = await readMarkdown(control);
|
|
1516
1603
|
const sourceSystems = (
|
|
1517
1604
|
modelSupports(model, "component-sources")
|
|
@@ -1523,6 +1610,18 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
1523
1610
|
&& record.status !== "retired"
|
|
1524
1611
|
&& (record.controlIds || []).includes(control.id)
|
|
1525
1612
|
));
|
|
1613
|
+
const sourceDetails = await Promise.all(sourceSystems.map(async (sourceRecord) => ({
|
|
1614
|
+
record: sourceRecord,
|
|
1615
|
+
instructions: await readMarkdown(sourceRecord)
|
|
1616
|
+
})));
|
|
1617
|
+
const missingSourceFamilies = controlFamilies.filter((family) => !sourceDetails.some(({ record, instructions }) => (
|
|
1618
|
+
record.status === "active"
|
|
1619
|
+
&& (record.evidenceSourceKinds || []).length > 0
|
|
1620
|
+
&& (!family.sourceKinds.length || family.sourceKinds.some((kind) => (record.evidenceSourceKinds || []).includes(kind)))
|
|
1621
|
+
&& (record.evidenceOwnerIds || []).length > 0
|
|
1622
|
+
&& substantiveMarkdown(instructions)
|
|
1623
|
+
&& openPlaceholderCount(instructions) === 0
|
|
1624
|
+
)));
|
|
1526
1625
|
const checks = {
|
|
1527
1626
|
...(model.resources.control?.fields?.applicabilityReview ? {
|
|
1528
1627
|
applicability: control.applicabilityReview?.decision === "applicable"
|
|
@@ -1534,6 +1633,9 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
1534
1633
|
scope: (control.systemIds || []).some((id) => scope.systems.some((system) => system.id === id)),
|
|
1535
1634
|
operationPattern: Boolean(control.operationPattern),
|
|
1536
1635
|
evidenceSource: sourceSystems.length > 0,
|
|
1636
|
+
...(modelSupports(model, "component-sources") ? {
|
|
1637
|
+
evidenceSourceReady: missingSourceFamilies.length === 0
|
|
1638
|
+
} : {}),
|
|
1537
1639
|
implementationDate: Boolean(control.effectiveOn && control.effectiveOn <= asOf),
|
|
1538
1640
|
...(model.resources.control?.fields?.procedureRevision ? {
|
|
1539
1641
|
procedureRevision: Boolean(control.procedureRevision),
|
|
@@ -1547,25 +1649,21 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
1547
1649
|
} : {})
|
|
1548
1650
|
};
|
|
1549
1651
|
const missing = Object.entries(checks).filter(([, value]) => !value).map(([name]) => controlCheckLabel(name));
|
|
1652
|
+
const nextSteps = controlImplementationSteps(control, checks, {
|
|
1653
|
+
missingSourceFamilies
|
|
1654
|
+
});
|
|
1550
1655
|
items.push(item(
|
|
1551
1656
|
`control-${control.id}`,
|
|
1552
1657
|
missing.length ? "action" : "complete",
|
|
1553
1658
|
`${control.code ? `${control.code}: ` : ""}${control.title}`,
|
|
1554
1659
|
missing.length
|
|
1555
|
-
? `Complete
|
|
1660
|
+
? nextSteps[0] || `Complete the remaining Control checks: ${missing.join(", ")}.`
|
|
1556
1661
|
: `Implemented ${control.effectiveOn}; owned, scoped, scheduled, documented, mapped, and tied to ${sourceSystems.length} authoritative ${sourceSystems.length === 1 ? "source" : "sources"}.`,
|
|
1557
1662
|
control,
|
|
1558
1663
|
{
|
|
1559
1664
|
checks,
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
"npx filegrc review-applicability --scaffold --type control > control-decisions.json",
|
|
1563
|
-
"npx filegrc review-applicability control-decisions.json --preview --json",
|
|
1564
|
-
"npx filegrc review-applicability control-decisions.json --yes --json"
|
|
1565
|
-
] : []),
|
|
1566
|
-
"npx filegrc evidence-map --json",
|
|
1567
|
-
`npx filegrc get ${shellArgument(control.id)} --mutation`
|
|
1568
|
-
],
|
|
1665
|
+
nextSteps,
|
|
1666
|
+
commands: [`npx filegrc get ${shellArgument(control.id)} --mutation`],
|
|
1569
1667
|
workQueue: queueSchedules.length ? {
|
|
1570
1668
|
enabled: queueSchedules.filter(obligationIsEnabled).length,
|
|
1571
1669
|
running: queueSchedules.filter((obligation) => obligationIsRunning(obligation, byId, asOf, model)).length,
|
|
@@ -1574,7 +1672,7 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
1574
1672
|
}
|
|
1575
1673
|
));
|
|
1576
1674
|
}
|
|
1577
|
-
return stage("controls", "Implement Controls", "
|
|
1675
|
+
return stage("controls", "Implement Controls", "Put each selected Control in place, record how it works, and mark it Implemented when it is working.", items);
|
|
1578
1676
|
}
|
|
1579
1677
|
|
|
1580
1678
|
async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
|
|
@@ -1930,6 +2028,7 @@ function controlCheckLabel(name) {
|
|
|
1930
2028
|
scope: "in-scope systems",
|
|
1931
2029
|
operationPattern: "operation pattern",
|
|
1932
2030
|
evidenceSource: "authoritative evidence source",
|
|
2031
|
+
evidenceSourceReady: "usable evidence source",
|
|
1933
2032
|
implementationDate: "implementation date",
|
|
1934
2033
|
procedureRevision: "effective procedure revision",
|
|
1935
2034
|
procedureEffective: "procedure effective date",
|
|
@@ -1939,6 +2038,76 @@ function controlCheckLabel(name) {
|
|
|
1939
2038
|
})[name] || name;
|
|
1940
2039
|
}
|
|
1941
2040
|
|
|
2041
|
+
const starterControlSetup = {
|
|
2042
|
+
"control-security-governance": "Name a reviewer outside day-to-day security ownership and set up a quarterly security review.",
|
|
2043
|
+
"control-policy-management": "Assign someone to maintain the Control list and a separate approver for policies and plans. Set up their review path.",
|
|
2044
|
+
"control-security-communication": "Set up how the approved reporting routes, policy changes, and security notices reach staff and outside parties.",
|
|
2045
|
+
"control-workforce-expectations": "Set up the checks, agreements, and policy acknowledgements required before someone receives sensitive access.",
|
|
2046
|
+
"control-security-training": "Choose how new and existing workers receive security training, acknowledge the current material, and get reminders.",
|
|
2047
|
+
"control-risk-assessment": "Use the Program's risk method to assess the scoped service and record the results in FileGRC Risk Assessments and Risks.",
|
|
2048
|
+
"control-monitoring-remediation": "Define how Control problems are reported, reviewed, and verified closed. Use FileGRC Findings for gaps that need separate follow-up.",
|
|
2049
|
+
"control-access-authorization": "Configure unique, least-privilege accounts in the identity system. Restrict account changes to authorized admins.",
|
|
2050
|
+
"control-strong-authentication": "Turn on required MFA and secure sign-in settings for the in-scope Systems. Identify any gap that needs an approved Exception.",
|
|
2051
|
+
"control-access-review-offboarding": "Confirm the identity system can produce a complete access list and admins can promptly remove or change permissions.",
|
|
2052
|
+
"control-physical-workspace-security": "Decide how visitors, work areas, devices, and paper records are protected in the places your team actually works.",
|
|
2053
|
+
"control-data-classification-inventory": "Apply the approved Classifications to important data in the in-scope Systems and record its location in the inventory.",
|
|
2054
|
+
"control-encryption-transmission": "Configure encryption for in-scope data and devices, and assign who manages keys and approved transfer methods.",
|
|
2055
|
+
"control-data-retention-disposal": "Apply the approved retention schedule to live, backup, and vendor-held data. Define how each class is deleted or destroyed.",
|
|
2056
|
+
"control-inventory-configuration": "List important assets and owners, choose their secure settings, and decide how unsupported assets are handled.",
|
|
2057
|
+
"control-endpoint-protection": "Apply the required device settings, updates, encryption, screen lock, and malware protection to devices that access company Systems.",
|
|
2058
|
+
"control-network-security": "Restrict production network paths and remote access, and document the rules that allow traffic between environments.",
|
|
2059
|
+
"control-change-management": "Set up a change path with risk review, testing, approval, deployment, and rollback for software and infrastructure changes.",
|
|
2060
|
+
"control-vulnerability-management": "Choose what will be scanned, how often, who handles findings, and the target times for fixing them.",
|
|
2061
|
+
"control-penetration-testing": "Decide whether independent testing is needed for this service. If it is, set its scope, cadence, and owner.",
|
|
2062
|
+
"control-logging-monitoring": "Enable useful security logs and alerts for important Systems, protect them, and choose who responds to alerts.",
|
|
2063
|
+
"control-incident-response": "Set up a way to report, assign, escalate, contain, and close security incidents using the approved response plan.",
|
|
2064
|
+
"control-incident-exercise": "Choose a realistic incident scenario and an alert path to test, and assign the people who will run the exercise.",
|
|
2065
|
+
"control-backup-restoration": "Choose what needs backup or another recovery path, configure it, and decide how restores will be tested.",
|
|
2066
|
+
"control-continuity-exercise": "Set recovery priorities, contacts, and responsibilities, and choose how the continuity plan will be exercised.",
|
|
2067
|
+
"control-vendor-due-diligence": "Set a security review and contract check before a new Vendor gets sensitive data or becomes a material dependency.",
|
|
2068
|
+
"control-vendor-monitoring": "Choose which Vendors need ongoing review, who reviews them, and what information the review uses.",
|
|
2069
|
+
"control-security-exceptions": "Document how an Exception is reviewed and approved before a departure from policy begins. Record actual decisions in FileGRC Exceptions."
|
|
2070
|
+
};
|
|
2071
|
+
|
|
2072
|
+
export function controlImplementationSteps(control, checks, options = {}) {
|
|
2073
|
+
const steps = [];
|
|
2074
|
+
if (!checks.implemented) steps.push(starterControlSetup[control.id]
|
|
2075
|
+
|| `Set up this Control: ${control.activity || control.statement}`);
|
|
2076
|
+
const record = [];
|
|
2077
|
+
if (!checks.owner) record.push("choose an owner");
|
|
2078
|
+
if (!checks.scope) record.push("select the Systems this covers");
|
|
2079
|
+
if (!checks.operationPattern) record.push("set when it runs");
|
|
2080
|
+
if (!checks.procedure) record.push("write who does what and when in Procedure");
|
|
2081
|
+
if (!checks.evidenceSource) record.push("link the tool or system that can show it happened");
|
|
2082
|
+
if (!checks.policyMapping && checks.policyMapping !== undefined) record.push("link the Policy it implements");
|
|
2083
|
+
if (!checks.criteriaMapping && checks.criteriaMapping !== undefined) record.push("link the criteria it covers");
|
|
2084
|
+
if (checks.implemented && !checks.implementationDate) record.push("enter the date it started working");
|
|
2085
|
+
if (checks.implemented && (checks.procedureRevision === false || checks.procedureEffective === false)) {
|
|
2086
|
+
record.push("record the Procedure revision and effective date");
|
|
2087
|
+
}
|
|
2088
|
+
if (record.length) steps.push(`In this Control, ${record.join("; ")}.`);
|
|
2089
|
+
const sourceFamilies = options.missingSourceFamilies || [];
|
|
2090
|
+
const sourceTargets = sourceFamilies.map((family) => `${family.title}${family.sourceKinds?.length ? ` (${family.sourceKinds.join(" or ")})` : ""}`);
|
|
2091
|
+
const sourceStep = checks.evidenceSourceReady === false
|
|
2092
|
+
? `Cover the missing evidence families: ${sourceTargets.length ? sourceTargets.join(" and ") : "this Control"}. For each, link an active Component with the matching kind, evidence owners, and report retrieval instructions.`
|
|
2093
|
+
: null;
|
|
2094
|
+
const queueStep = checks.workQueue === false
|
|
2095
|
+
? "Enable a linked Obligation with the owner and schedule for this Control's recurring or event work."
|
|
2096
|
+
: null;
|
|
2097
|
+
if (!checks.implemented) {
|
|
2098
|
+
if (sourceStep || queueStep) steps.push([sourceStep, queueStep].filter(Boolean).join(" "));
|
|
2099
|
+
else if (checks.applicability === false) steps.push("Confirm this Control applies to the current Program scope with `review-applicability --scaffold --type control`.");
|
|
2100
|
+
else steps.push(checks.procedureRevision === undefined
|
|
2101
|
+
? "When it works, mark this Control Implemented and enter its start date."
|
|
2102
|
+
: "When it works, mark this Control Implemented. Enter its start date and the Procedure revision and effective date.");
|
|
2103
|
+
} else {
|
|
2104
|
+
if (sourceStep) steps.push(sourceStep);
|
|
2105
|
+
if (queueStep) steps.push(queueStep);
|
|
2106
|
+
if (checks.applicability === false) steps.push("Confirm this Control applies to the current Program scope with `review-applicability --scaffold --type control`.");
|
|
2107
|
+
}
|
|
2108
|
+
return steps;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
1942
2111
|
function assuranceGoalLabel(goal) {
|
|
1943
2112
|
if (goal === "soc-2-type-1") return "SOC 2 Type 1";
|
|
1944
2113
|
if (goal === "soc-2-type-2") return "SOC 2 Type 2";
|
package/src/reconciliation.js
CHANGED
|
@@ -769,6 +769,7 @@ function validateMergeIdentities(index, model) {
|
|
|
769
769
|
const identities = new Map();
|
|
770
770
|
for (const parent of parents) {
|
|
771
771
|
const state = recordStateAtCommit(index, model, parent);
|
|
772
|
+
if (!state) continue;
|
|
772
773
|
let snapshot = snapshots.get(state);
|
|
773
774
|
if (!snapshot) {
|
|
774
775
|
snapshot = state.snapshot || snapshotRecordState(state);
|
|
@@ -43,7 +43,7 @@ export function retentionScheduleApprovalIssues(loaded, program, rows, options =
|
|
|
43
43
|
|| !substantiveMarkdown(scheduleContent)
|
|
44
44
|
|| openPlaceholderCount(scheduleContent) > 0)
|
|
45
45
|
) {
|
|
46
|
-
issues.push(issue("incomplete-retention-schedule-document", "Complete the Data Retention Schedule document, its owner, linked Control, and Markdown before requesting approval of the complete schedule."));
|
|
46
|
+
issues.push(issue("incomplete-retention-schedule-document", "Complete the Data Retention Schedule document, its owner, linked Control, and Markdown before requesting approval of the complete schedule.", schedule.id));
|
|
47
47
|
}
|
|
48
48
|
if (
|
|
49
49
|
schedule
|
|
@@ -60,7 +60,7 @@ export function retentionScheduleApprovalIssues(loaded, program, rows, options =
|
|
|
60
60
|
? activeRows.filter(({ scheduleDocumentId }) => scheduleDocumentId !== schedule.id)
|
|
61
61
|
: [];
|
|
62
62
|
if (mismatched.length) {
|
|
63
|
-
issues.push(issue("retention-row-wrong-schedule-document", `Link every active retention row to the current Data Retention Schedule document before approval: ${mismatched[0].title}
|
|
63
|
+
issues.push(issue("retention-row-wrong-schedule-document", `Link every active retention row to the current Data Retention Schedule document before approval: ${mismatched[0].title}.`, mismatched[0].id));
|
|
64
64
|
}
|
|
65
65
|
const revisions = resourceReviewRevisionsSync(
|
|
66
66
|
loaded,
|
|
@@ -68,7 +68,7 @@ export function retentionScheduleApprovalIssues(loaded, program, rows, options =
|
|
|
68
68
|
);
|
|
69
69
|
const incomplete = activeRows.filter((row) => !retentionRuleIsCurrent(row, revisions, byId, loaded));
|
|
70
70
|
if (incomplete.length) {
|
|
71
|
-
issues.push(issue("incomplete-retention-schedule-row", `Complete the retention decisions before approving the schedule revision: ${incomplete[0].title}
|
|
71
|
+
issues.push(issue("incomplete-retention-schedule-row", `Complete the retention decisions before approving the schedule revision: ${incomplete[0].title}.`, incomplete[0].id));
|
|
72
72
|
}
|
|
73
73
|
const proposedRows = rows.filter(rowProposesCoverage);
|
|
74
74
|
const scopedPrograms = program.programIds
|
|
@@ -96,8 +96,8 @@ function sameIds(left = [], right = []) {
|
|
|
96
96
|
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function issue(code, message) {
|
|
100
|
-
return { code, message };
|
|
99
|
+
function issue(code, message, resourceId) {
|
|
100
|
+
return { code, message, ...(resourceId ? { resourceId } : {}) };
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function primaryMarkdown(loaded, record) {
|
package/src/state.js
CHANGED
|
@@ -277,6 +277,7 @@ async function createAppStateUnlocked(input, options) {
|
|
|
277
277
|
(audits.length ? audits : [null]).map(async (audit) => {
|
|
278
278
|
const preparation = await assessAuditPreparation(loaded, {
|
|
279
279
|
auditId: audit?.id,
|
|
280
|
+
programId: activeProgram.id,
|
|
280
281
|
asOf,
|
|
281
282
|
generatedAt,
|
|
282
283
|
...(audit ? {} : { programReadiness })
|