filegrc 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/model/index.js +21 -4
- package/model/v4.json +97 -6
- package/model/v5.json +10233 -0
- package/package.json +4 -2
- package/src/agent.js +4 -3
- package/src/audit-preparation.js +635 -81
- package/src/audit-transition.js +8 -2
- package/src/batch-review.js +21 -11
- package/src/cli.js +117 -12
- package/src/collection-review.js +4 -3
- package/src/collection-scope.js +4 -3
- package/src/document-activation.js +145 -0
- package/src/evidence-packet.js +393 -106
- package/src/external-reviewer.js +5 -4
- package/src/files.js +194 -35
- package/src/git.js +106 -10
- package/src/index.js +10 -1
- package/src/model-migration.js +218 -7
- package/src/obligations.js +14 -11
- package/src/policy-library/information-security-policy-v2.md +290 -0
- package/src/policy-library.js +827 -0
- package/src/program-lifecycle.js +93 -3
- package/src/program-path.js +14 -9
- package/src/program-readiness.js +306 -75
- package/src/program.js +5 -3
- package/src/reconciliation.js +3 -2
- package/src/server.js +29 -7
- package/src/setup.js +25 -5
- package/src/soc2.js +228 -0
- package/src/state.js +8 -0
- package/src/validate.js +168 -14
- package/src/web.js +257 -35
- package/src/workflow.js +107 -44
- package/src/workspace.js +5 -0
package/src/workflow.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cp, mkdtemp, rm } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { modelSupports } from "../model/index.js";
|
|
4
5
|
import { assessRequiredAppointments } from "./appointments.js";
|
|
5
6
|
import { assessSourceCoverageReadiness } from "./source-coverage.js";
|
|
6
7
|
import { coverageEnd, coverageStart } from "./coverage.js";
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
import { assessAuditPreparation } from "./audit-preparation.js";
|
|
22
23
|
import { assessProgramReadiness } from "./program-readiness.js";
|
|
23
24
|
import { resolveProgram } from "./program.js";
|
|
25
|
+
import { signatoryAppointmentIssue, soc2ReportEvidenceIssue, subsequentEventsReviewIssue } from "./soc2.js";
|
|
24
26
|
import { planReconciliation } from "./reconciliation.js";
|
|
25
27
|
import { currentCalendarDate } from "./time.js";
|
|
26
28
|
import { measureTiming } from "./timing.js";
|
|
@@ -92,7 +94,7 @@ async function assessWorkflowUnmeasured(input, options = {}) {
|
|
|
92
94
|
if (options.auditId && selectedAuditRecords[0]?.programId && selectedAuditRecords[0].programId !== programRecord.id) {
|
|
93
95
|
throw new Error(`Audit "${options.auditId}" belongs to Program "${selectedAuditRecords[0].programId}", not "${programRecord.id}".`);
|
|
94
96
|
}
|
|
95
|
-
const audits =
|
|
97
|
+
const audits = modelSupports(loaded.model, "program-scope")
|
|
96
98
|
? selectedAuditRecords.filter(({ programId }) => programId === programRecord.id)
|
|
97
99
|
: selectedAuditRecords;
|
|
98
100
|
const timezone = options.timezone || workspace?.timezone || "UTC";
|
|
@@ -304,15 +306,24 @@ function programFindings(program) {
|
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
function auditFindings(preparation) {
|
|
307
|
-
return preparation.stages.flatMap((stage) => stage.items.map((item) =>
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
309
|
+
return preparation.stages.flatMap((stage) => stage.items.map((item) => {
|
|
310
|
+
const finding = normalizeFinding(
|
|
311
|
+
`audit.${preparation.audit?.id || "unscoped"}.${stage.id}.${item.id}`,
|
|
312
|
+
item,
|
|
313
|
+
{
|
|
314
|
+
assessment: stage.id === "auditor" ? "audit-closure" : "audit-readiness",
|
|
315
|
+
stage: stage.id,
|
|
316
|
+
auditId: preparation.audit?.id || null
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
if (
|
|
320
|
+
finding.state === "ready"
|
|
321
|
+
&& !finding.actions.length
|
|
322
|
+
&& finding.subject?.type === "audit"
|
|
323
|
+
&& preparation.audit
|
|
324
|
+
) finding.actions = [mutationAction(preparation.audit)];
|
|
325
|
+
return finding;
|
|
326
|
+
}));
|
|
316
327
|
}
|
|
317
328
|
|
|
318
329
|
function validationFindings(validation, loaded) {
|
|
@@ -623,7 +634,7 @@ function appointmentFinding(kind, template, record, state, requiredness) {
|
|
|
623
634
|
}
|
|
624
635
|
|
|
625
636
|
function sourceCoverageFindings(loaded, program) {
|
|
626
|
-
const selected =
|
|
637
|
+
const selected = modelSupports(loaded.model, "program-scope")
|
|
627
638
|
? new Set(program?.controlIds || [])
|
|
628
639
|
: null;
|
|
629
640
|
const selectedControlIds = loaded.resources
|
|
@@ -870,8 +881,9 @@ async function assessPeriodHealth(loaded, options) {
|
|
|
870
881
|
}
|
|
871
882
|
|
|
872
883
|
function auditLifecycleFindings(loaded, audits, program) {
|
|
873
|
-
if (!
|
|
884
|
+
if (!modelSupports(loaded.model, "guided-workflow")) return [];
|
|
874
885
|
const target = program || resolveProgram(loaded);
|
|
886
|
+
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
875
887
|
const findings = [];
|
|
876
888
|
for (const audit of audits) {
|
|
877
889
|
if (!(audit.controlIds || []).length) {
|
|
@@ -917,45 +929,54 @@ function auditLifecycleFindings(loaded, audits, program) {
|
|
|
917
929
|
));
|
|
918
930
|
}
|
|
919
931
|
}
|
|
920
|
-
if (!["planning", "draft"].includes(audit.status)) {
|
|
921
|
-
if (!audit.engagementTermsDocumentId) {
|
|
922
|
-
findings.push(auditLifecycleFinding(
|
|
923
|
-
audit,
|
|
924
|
-
"engagement-terms",
|
|
925
|
-
"audit-readiness",
|
|
926
|
-
"Link the engagement terms",
|
|
927
|
-
"Record the CPA firm's engagement terms without representing its professional judgments as management facts."
|
|
928
|
-
));
|
|
929
|
-
}
|
|
930
|
-
if (!(audit.managementAcknowledgedByIds || []).length || !audit.managementAcknowledgedOn) {
|
|
931
|
-
findings.push(auditLifecycleFinding(
|
|
932
|
-
audit,
|
|
933
|
-
"management-acknowledgement",
|
|
934
|
-
"audit-readiness",
|
|
935
|
-
"Record management acknowledgement",
|
|
936
|
-
"Name who acknowledged the engagement terms and the actual acknowledgement date."
|
|
937
|
-
));
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
932
|
const lateStage = ["report-draft", "issued", "delivered", "complete"].includes(audit.status);
|
|
941
|
-
|
|
933
|
+
const subsequentEventsIssue = lateStage ? subsequentEventsReviewIssue(audit) : null;
|
|
934
|
+
if (subsequentEventsIssue) {
|
|
942
935
|
findings.push(auditLifecycleFinding(
|
|
943
936
|
audit,
|
|
944
937
|
"subsequent-events",
|
|
945
938
|
"audit-readiness",
|
|
946
939
|
"Complete the subsequent-events review",
|
|
947
|
-
|
|
940
|
+
subsequentEventsIssue.message
|
|
948
941
|
));
|
|
949
942
|
}
|
|
950
|
-
|
|
943
|
+
const signatoryIssue = modelSupports(loaded.model, "program-scope") && lateStage
|
|
944
|
+
? signatoryAppointmentIssue(audit, byId)
|
|
945
|
+
: null;
|
|
946
|
+
if (signatoryIssue) {
|
|
947
|
+
findings.push(auditLifecycleFinding(
|
|
948
|
+
audit,
|
|
949
|
+
"signatory-authority",
|
|
950
|
+
"audit-readiness",
|
|
951
|
+
"Confirm authorized signatories",
|
|
952
|
+
signatoryIssue.message
|
|
953
|
+
));
|
|
954
|
+
}
|
|
955
|
+
const packetIssue = ["fieldwork", "report-draft", "issued", "delivered", "complete"].includes(audit.status)
|
|
956
|
+
? packetDeliveryIssue(audit.packetDelivery)
|
|
957
|
+
: null;
|
|
958
|
+
if (packetIssue) {
|
|
951
959
|
findings.push(auditLifecycleFinding(
|
|
952
960
|
audit,
|
|
953
961
|
"packet-delivery",
|
|
954
962
|
"delivery-readiness",
|
|
955
963
|
"Approve and record packet delivery",
|
|
956
|
-
|
|
964
|
+
packetIssue
|
|
957
965
|
));
|
|
958
966
|
}
|
|
967
|
+
if (["issued", "delivered", "complete"].includes(audit.status)) {
|
|
968
|
+
const reportEvidence = loaded.resources.find((record) => record.id === audit.reportEvidenceId);
|
|
969
|
+
const reportIssue = soc2ReportEvidenceIssue(reportEvidence, audit, loaded.model.modelVersion);
|
|
970
|
+
if (reportIssue) {
|
|
971
|
+
findings.push(auditLifecycleFinding(
|
|
972
|
+
audit,
|
|
973
|
+
"report-evidence",
|
|
974
|
+
"audit-closure",
|
|
975
|
+
"Link the exact issued SOC 2 report",
|
|
976
|
+
reportIssue.message
|
|
977
|
+
));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
959
980
|
if (audit.status !== "complete") {
|
|
960
981
|
const nextStep = auditClosureNextStep(audit.status);
|
|
961
982
|
findings.push(auditLifecycleFinding(
|
|
@@ -991,10 +1012,8 @@ function auditLifecycleFindings(loaded, audits, program) {
|
|
|
991
1012
|
));
|
|
992
1013
|
}
|
|
993
1014
|
for (const [field, title, message] of [
|
|
994
|
-
["reportEvidenceId", "Link the issued report", "Link the exact issued report evidence and its coverage."],
|
|
995
1015
|
["retentionDecision", "Record the retention decision", "Record the approved retention and authorized distribution decision."],
|
|
996
|
-
["carryForwardActionIds", "Review carry-forward work", "Record the next-period actions, including an explicit empty list when no carry-forward work remains."]
|
|
997
|
-
["signatoryAppointmentIds", "Confirm authorized signatories", "Link active authority Appointments for management assertion and representation signers."]
|
|
1016
|
+
["carryForwardActionIds", "Review carry-forward work", "Record the next-period actions, including an explicit empty list when no carry-forward work remains."]
|
|
998
1017
|
]) {
|
|
999
1018
|
if (field === "carryForwardActionIds" ? Array.isArray(audit[field]) : present(audit[field])) continue;
|
|
1000
1019
|
findings.push(auditLifecycleFinding(audit, field, "audit-closure", title, message));
|
|
@@ -1003,6 +1022,41 @@ function auditLifecycleFindings(loaded, audits, program) {
|
|
|
1003
1022
|
return findings;
|
|
1004
1023
|
}
|
|
1005
1024
|
|
|
1025
|
+
export function packetDeliveryIssue(delivery) {
|
|
1026
|
+
if (!delivery) {
|
|
1027
|
+
return "Record the least-disclosure review, redaction decision, recipient, delivery system, exact packet revision and manifest, management approval, delivery date, and receipt.";
|
|
1028
|
+
}
|
|
1029
|
+
if (!(delivery.classificationReviewedByIds || []).length || !(delivery.approvedByIds || []).length) {
|
|
1030
|
+
return "Name the people who performed the least-disclosure review and approved the delivery.";
|
|
1031
|
+
}
|
|
1032
|
+
for (const [field, label] of [
|
|
1033
|
+
["redactionDecision", "redaction decision"],
|
|
1034
|
+
["recipient", "recipient"],
|
|
1035
|
+
["deliverySystem", "delivery system"],
|
|
1036
|
+
["packetCommit", "packet Git revision"],
|
|
1037
|
+
["manifestChecksum", "manifest checksum"],
|
|
1038
|
+
["receiptReference", "delivery receipt reference"]
|
|
1039
|
+
]) {
|
|
1040
|
+
if (!String(delivery[field] || "").trim()) return `Record the ${label} for the delivered packet.`;
|
|
1041
|
+
}
|
|
1042
|
+
if (!/^[a-f0-9]{40}$/i.test(delivery.packetCommit)) {
|
|
1043
|
+
return "Record the delivered packet's exact 40-character Git commit.";
|
|
1044
|
+
}
|
|
1045
|
+
if (!/^(?:sha256:)?[a-f0-9]{64}$/i.test(delivery.manifestChecksum)) {
|
|
1046
|
+
return "Record the packet manifest checksum as a SHA-256 digest.";
|
|
1047
|
+
}
|
|
1048
|
+
if (
|
|
1049
|
+
!delivery.classificationReviewedOn
|
|
1050
|
+
|| !delivery.approvedOn
|
|
1051
|
+
|| !delivery.deliveredOn
|
|
1052
|
+
|| delivery.classificationReviewedOn > delivery.approvedOn
|
|
1053
|
+
|| delivery.approvedOn > delivery.deliveredOn
|
|
1054
|
+
) {
|
|
1055
|
+
return "Record chronological least-disclosure review, management approval, and delivery dates.";
|
|
1056
|
+
}
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1006
1060
|
function normalizeFinding(code, item, context) {
|
|
1007
1061
|
const state = findingState(item.status);
|
|
1008
1062
|
const subject = item.resourceType
|
|
@@ -1186,6 +1240,7 @@ function buildAssessments({ program, audits, auditPreparations, obligationPlan,
|
|
|
1186
1240
|
&& !findings.some((finding) => finding.assessment === "audit-closure" && blockingFinding(finding));
|
|
1187
1241
|
const deliveryFindingKeys = findingKeys(findings, "delivery-readiness");
|
|
1188
1242
|
const policyActivations = program.policyActivations || [];
|
|
1243
|
+
const documentActivations = program.documentActivations || [];
|
|
1189
1244
|
const policiesOperating = policyActivations.length > 0
|
|
1190
1245
|
&& policyActivations.every(({ state }) => state === "active-and-operating");
|
|
1191
1246
|
return {
|
|
@@ -1216,6 +1271,18 @@ function buildAssessments({ program, audits, auditPreparations, obligationPlan,
|
|
|
1216
1271
|
findingKeys: findingKeys(findings, "program-configuration", ({ key }) => key.includes(".policy-activation-")),
|
|
1217
1272
|
policies: policyActivations
|
|
1218
1273
|
},
|
|
1274
|
+
documentActivation: {
|
|
1275
|
+
status: documentActivations.length && documentActivations.every(({ state }) => state === "active-and-operating")
|
|
1276
|
+
? "complete"
|
|
1277
|
+
: documentActivations.length ? "needs-work" : "not-started",
|
|
1278
|
+
message: documentActivations.length && documentActivations.every(({ state }) => state === "active-and-operating")
|
|
1279
|
+
? "Every required governed Document is separately approved, activated, effective, and operating."
|
|
1280
|
+
: documentActivations.length
|
|
1281
|
+
? "Approve required plans and schedules in Step 2, implement their linked requirements, then activate their exact revisions in Step 3."
|
|
1282
|
+
: "No required governed Document activation is configured.",
|
|
1283
|
+
findingKeys: findingKeys(findings, "program-configuration", ({ key }) => key.includes(".document-")),
|
|
1284
|
+
documents: documentActivations
|
|
1285
|
+
},
|
|
1219
1286
|
policyLibraryReview: {
|
|
1220
1287
|
status: program.policyLibraryProposals?.length ? "review" : "current",
|
|
1221
1288
|
message: program.policyLibraryProposals?.length
|
|
@@ -1451,14 +1518,10 @@ function periodFinding(key, title, message, state, subject, actions, dependencie
|
|
|
1451
1518
|
|
|
1452
1519
|
function auditClosureNextStep(status) {
|
|
1453
1520
|
const steps = {
|
|
1454
|
-
|
|
1521
|
+
planned: {
|
|
1455
1522
|
title: "Confirm the engagement and start audit preparation",
|
|
1456
1523
|
message: "Link the agreed engagement terms, confirm scope and management acknowledgement, then move the audit to in progress."
|
|
1457
1524
|
},
|
|
1458
|
-
draft: {
|
|
1459
|
-
title: "Finalize the draft engagement",
|
|
1460
|
-
message: "Resolve the draft scope and ownership, link the agreed engagement terms, then move the audit to in progress."
|
|
1461
|
-
},
|
|
1462
1525
|
"in-progress": {
|
|
1463
1526
|
title: "Begin fieldwork",
|
|
1464
1527
|
message: "Finish management preparation, confirm the fieldwork dates, and move the audit to fieldwork when the CPA firm begins testing."
|
package/src/workspace.js
CHANGED
|
@@ -2,8 +2,13 @@ import { readFile, readdir } from "node:fs/promises";
|
|
|
2
2
|
import { join, relative, sep } from "node:path";
|
|
3
3
|
import { loadModel } from "../model/index.js";
|
|
4
4
|
import { resolveWorkspaceRoot } from "./paths.js";
|
|
5
|
+
import { measureTiming } from "./timing.js";
|
|
5
6
|
|
|
6
7
|
export async function loadWorkspace(input = process.cwd()) {
|
|
8
|
+
return measureTiming("workspace-load", () => loadWorkspaceUnmeasured(input));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function loadWorkspaceUnmeasured(input) {
|
|
7
12
|
const root = resolveWorkspaceRoot(input);
|
|
8
13
|
const dataRoot = join(root, "data");
|
|
9
14
|
const diagnostics = [];
|