diff --git a/components/sctm/SecurityAssessment.vue b/components/sctm/SecurityAssessment.vue index a83e508..235d199 100644 --- a/components/sctm/SecurityAssessment.vue +++ b/components/sctm/SecurityAssessment.vue @@ -255,7 +255,7 @@ \ No newline at end of file diff --git a/pages/company-boundary/[boundaryId]/SCTM/[sctmId].vue b/pages/company-boundary/[boundaryId]/SCTM/[sctmId].vue index 893ddf2..19a8f00 100644 --- a/pages/company-boundary/[boundaryId]/SCTM/[sctmId].vue +++ b/pages/company-boundary/[boundaryId]/SCTM/[sctmId].vue @@ -287,6 +287,35 @@ + + + Technical Assessment + + + + + + + + ; // ControlStatements OR ControlEnhancementStatements - type: "control" | "enhancement"; + statements: string[]; + cci: string; + technicalAssessmentStatus: string; + technicalAssessmentComments: string[]; +} +interface NewRecord { + BoundaryId: number; + ControlFamilyId: number; + ControlRevisionId: number; + lastUpdate: string; + creationDate: string; +} + +interface NewItem { + ControlRecordId: number; + ControlId: number; + ComplianceStatusId: number; + ControlEnhancementId?: number; + AssessorControlStatusId: number; + AuditControlStatusId: number; + lastUpdate: string; + creationDate: string; +} + +interface InsertedControlRecord { + id: number; + ControlFamilyId: number; } export async function createControlRecords(BoundaryId: number, controlRev: string) { const revName = `rev${controlRev}`; const now = DateTime.now().toISO(); - // Get revision - const revision = await ControlRevision.findOne({ where: { name: revName } }); + + const revision = await ControlRevision.findOne({ + where: { name: revName }, + attributes: ["id"], + }); if (!revision) { - console.warn(`Revision ${revName} not found. Skipping...`); - return; // just exit the function early + throw createError({ + statusCode: 404, + statusMessage: `Control Revision ${revName} not found.`, + }); } - // Default compliance status - const notReviewedStatus = await ComplianceStatus.findOne({ where: { status: "Not Reviewed" } }); - if (!notReviewedStatus) throw new Error(`Not Reviewed compliance status not found`); + const notReviewedStatus = await ComplianceStatus.findOne({ + where: { status: "Not Reviewed" }, + attributes: ["id"], + }); + if (!notReviewedStatus) { + throw createError({ + statusCode: 404, + statusMessage: "Not Reviewed compliance status not found.", + }); + } - // Get all control families with their controls + enhancements for this revision const controlFamilies = await ControlFamily.findAll({ + attributes: ["id"], include: [ { model: Control, required: true, + attributes: ["id", "ControlFamilyId"], include: [ { model: ControlRevision, where: { id: revision.id }, required: true, + attributes: [], }, { model: ControlEnhancement, + attributes: ["id", "ControlId"], }, ], }, ], }); - // Fetch existing ControlRecords for this boundary + revision to avoid duplicates const existingRecords = await ControlRecord.findAll({ where: { BoundaryId, ControlRevisionId: revision.id }, attributes: ["id", "ControlFamilyId"], }); const existingRecordMap = new Map(existingRecords.map((r) => [r.ControlFamilyId, r.id])); - const newRecords: any[] = []; - const newItems: any[] = []; + const newRecords: NewRecord[] = []; + const newItems: NewItem[] = []; for (const family of controlFamilies) { const recordId = existingRecordMap.get(family.id); if (!recordId) { - // Prepare new ControlRecord newRecords.push({ BoundaryId, ControlFamilyId: family.id, @@ -81,87 +176,318 @@ export async function createControlRecords(BoundaryId: number, controlRev: strin } } - // Bulk insert new ControlRecords and get their IDs - let insertedRecords: any[] = []; - if (newRecords.length > 0) { - try { - insertedRecords = await ControlRecord.bulkCreate(newRecords, { returning: true }); - insertedRecords.forEach((r: any) => existingRecordMap.set(r.ControlFamilyId, r.id)); - } catch (err: any) { - if (err.name === "SequelizeValidationError") { - console.error( - "Validation error messages:", - err.errors.map((e: any) => e.message), - ); - console.error( - "Invalid fields:", - err.errors.map((e: any) => e.path), - ); - } else if (err.name === "SequelizeForeignKeyConstraintError") { - console.error("Foreign key constraint error:", err.fields); - } else { - console.error("Unexpected error inserting ControlRecords:", err); + try { + await sequelize.transaction(async (t) => { + let insertedRecords: InsertedControlRecord[] = []; + if (newRecords.length > 0) { + insertedRecords = await ControlRecord.bulkCreate(newRecords, { + returning: ["id", "ControlFamilyId"], + transaction: t, + }); + insertedRecords.forEach((r) => existingRecordMap.set(r.ControlFamilyId, r.id)); } - } + + for (const family of controlFamilies) { + const recordId = existingRecordMap.get(family.id); + if (!recordId) continue; + const controls = family.Controls ?? []; + for (const control of controls) { + newItems.push({ + ControlRecordId: recordId, + ControlId: control.id, + ComplianceStatusId: notReviewedStatus.id, + AssessorControlStatusId: notReviewedStatus.id, + AuditControlStatusId: notReviewedStatus.id, + lastUpdate: now, + creationDate: now, + }); + for (const enhancement of control.ControlEnhancements || []) { + newItems.push({ + ControlRecordId: recordId, + ControlId: control.id, + ControlEnhancementId: enhancement.id, + ComplianceStatusId: notReviewedStatus.id, + AssessorControlStatusId: notReviewedStatus.id, + AuditControlStatusId: notReviewedStatus.id, + lastUpdate: now, + creationDate: now, + }); + } + } + } + + if (newItems.length > 0) { + await ControlRecordItem.bulkCreate(newItems, { transaction: t }); + } + }); + + logger.info({ + service: "SCTM", + message: `ControlRecords and items ensured for boundary ${BoundaryId} and revision ${revName}`, + }); + } catch (err: any) { + logger.error({ + service: "SCTM", + message: `Failed to create control records for boundary ${BoundaryId} rev ${revName}`, + error: err.message, + }); + throw createError({ + statusCode: 500, + statusMessage: `Failed to create control records for boundary ${BoundaryId} rev ${revName}`, + }); } +} - // Prepare ControlRecordItems - for (const family of controlFamilies) { - const recordId = existingRecordMap.get(family.id); - if (!recordId) continue; - const controls = family.Controls ?? []; - for (const control of controls) { - // Base control item - newItems.push({ - ControlRecordId: recordId, - ControlId: control.id, - ComplianceStatusId: notReviewedStatus.id, - AssessorControlStatusId: notReviewedStatus.id, - AuditControlStatusId: notReviewedStatus.id, - lastUpdate: now, - creationDate: now, - }); +function getBaseFields(item: any): BaseFields { + return { + ControlRecordItemId: item.id, + family: item.Control.ControlFamily?.name || "", + ComplianceStatusId: item.ComplianceStatusId, + ImplementationStatusId: item.ImplementationStatusId, + CommonControlProviderId: item.CommonControlProviderId, + systemProvider: item.systemProvider, + SecurityControlDesignationId: item.SecurityControlDesignationId, + TestMethodId: item.TestMethodId, + naJustification: item.naJustification, + estimatedCompletionDate: item.estimatedCompletionDate, + implementationNarrative: item.implementationNarrative, + responsibleEntities: item.responsibleEntities, + criticality: item.criticality, + FrequencyTypeId: item.FrequencyTypeId, + ConMonMethodId: item.ConMonMethodId, + reporting: item.reporting, + tracking: item.tracking, + conmonComments: item.conmonComments, + SeverityId: item.SeverityId, + RelevanceOfThreatId: item.RelevanceOfThreatId, + LikelihoodId: item.LikelihoodId, + ImpactId: item.ImpactId, + ResidualRiskLevelId: item.ResidualRiskLevelId, + vulnerabilitySummary: item.vulnerabilitySummary, + mitigations: item.mitigations, + impactDescription: item.impactDescription, + recommendations: item.recommendations, + auditor: item.auditor, + AuditControlStatusId: item.AuditControlStatusId, + auditDate: item.auditDate, + auditComments: item.auditComments, + assessor: item.assessor, + AssessorControlStatusId: item.AssessorControlStatusId, + assessorDate: item.assessorDate, + assessorComments: item.assessorComments, + lastUpdate: item.lastUpdate, + creationDate: item.creationDate, + }; +} - // Enhancements - for (const enhancement of control.ControlEnhancements || []) { - newItems.push({ - ControlRecordId: recordId, - ControlId: control.id, - ControlEnhancementId: enhancement.id, - ComplianceStatusId: notReviewedStatus.id, - AssessorControlStatusId: notReviewedStatus.id, - AuditControlStatusId: notReviewedStatus.id, - lastUpdate: now, - creationDate: now, - }); +function getStandardControlSummary( + item: any, + cci: string, + technicalAssessmentStatus: string, + technicalAssessmentComments: string[], +): ControlSummary { + const baseFields = getBaseFields(item); + return { + type: "control", + id: item.Control.id, + number: item.Control.ControlNumber?.number || "", + title: item.Control.title, + revision: item.Control.ControlRevision?.name || "", + guidance: item.Control.guidance, + statements: item.Control.ControlStatements || [], + ...baseFields, + cci, + technicalAssessmentStatus, + technicalAssessmentComments, + }; +} + +function getEnhancementSummary( + item: any, + cci: string, + technicalAssessmentStatus: string, + technicalAssessmentComments: string[], +): EnhancementSummary { + const baseFields = getBaseFields(item); + return { + type: "enhancement", + id: item.ControlEnhancement.id, + number: item.ControlEnhancement.enhancementIdentifier, + title: item.ControlEnhancement.title, + revision: "", // enhancements may not have a revision + guidance: item.ControlEnhancement.guidance, + statements: item.ControlEnhancement.ControlEnhancementStatements || [], + ...baseFields, + cci, + technicalAssessmentStatus, + technicalAssessmentComments, + }; +} + +function evaluateStatuses(statusMap: Map>): string { + if (!statusMap || statusMap.size === 0) { + return "Not-Applicable"; + } + + const statuses = Array.from(statusMap.keys()); + if (statuses.some((status) => status.trim() === "Open")) { + return "Non-Compliant"; + } else if (statuses.some((status) => status.trim() === "Not_Reviewed")) { + return "Not Reviewed"; + } else if (statuses.some((status) => status.trim() === "NotAFinding")) { + return "Compliant"; + } else if (statuses.some((status) => status.trim() === "Not_Applicable")) { + return "Not-Applicable"; + } else { + return "Not-Applicable"; + } +} + +function buildDisplayLines(statusMap: Map>): string[] { + const lines: string[] = []; + for (const [status, vKeys] of statusMap) { + lines.push(status, ...Array.from(vKeys), ""); + } + return lines; +} + +function determineCciStatus(cciVKeys: { vKey: string; status: string }[]): string { + if (cciVKeys.some((cciVKey) => cciVKey.status === "Open")) { + return "Non-Compliant"; + } else if (cciVKeys.some((cciVKey) => cciVKey.status === "Not_Reviewed")) { + return "Not Reviewed"; + } else if (cciVKeys.some((cciVKey) => cciVKey.status === "NotAFinding")) { + return "Compliant"; + } else { + return "Not-Applicable"; + } +} + +interface TechnicalAssessment { + cci: string; + technicalAssessmentStatus: string; + technicalAssessmentComments: string[]; +} + +function getTechnicalAssessment( + normalizedControlNumber: string, + controlToStatusMap: Map>>, + cciMap: Map, + stigLookup: Record; stig: any }>, +): TechnicalAssessment { + const statusMap = controlToStatusMap.get(normalizedControlNumber); + const cciIds = cciMap.get(normalizedControlNumber) ?? []; + + if (!statusMap || statusMap.size === 0) { + return { + cci: cciIds.map((cciId) => ` ${cciId}: Not-Applicable`).join("\n"), + technicalAssessmentStatus: "Not-Applicable", + technicalAssessmentComments: [ + "No applicable STIG mapping for this Control.", + ], + }; + } + + const cciLines: string[] = []; + + for (const cciId of cciIds) { + const cciVKeys: { vKey: string; status: string }[] = []; + + for (const [status, vKeys] of statusMap) { + for (const displayValue of vKeys) { + const vKey = displayValue.split(" - ")[0]; + const stigEntry = stigLookup[vKey]; + + if (stigEntry?.cciIds.has(cciId)) { + cciVKeys.push({ vKey, status }); + } } } - } - // Optional: skip inserting if newItems is empty - if (newItems.length > 0) { - await ControlRecordItem.bulkCreate(newItems); + const cciStatus = + cciVKeys.length > 0 + ? determineCciStatus(cciVKeys) + : "Not-Applicable"; + + cciLines.push(` ${cciId}: ${cciStatus}`); } - logger.info({ - service: "SCTM", - message: `ControlRecords and items ensured for boundary ${BoundaryId} and revision ${revName}`, + return { + cci: cciLines.join("\n"), + technicalAssessmentStatus: evaluateStatuses(statusMap), + technicalAssessmentComments: buildDisplayLines(statusMap), + }; +} + +function parseTopLevelControl(raw: string): string { + if (!raw) return ""; + + const cleaned = raw.toUpperCase().replace(/\s+/g, ""); + const match = cleaned.match(/^([A-Z]{2,3}-\d+)(\(\d+\))?/); + + return match + ? match[1] + (match[2] ?? "") + : cleaned; +} + +async function loadCciItems(policyDocumentId: number | undefined) { + return CciItem.findAll({ + attributes: ["cciId", "definition"], + include: [ + { + model: CciReference, + attributes: ["index"], + through: { attributes: [] }, + where: { + PolicyDocumentId: policyDocumentId, + }, + }, + ], }); } +function buildCciMaps(cciItems: Awaited>) { + const cciItemMap = new Map( + cciItems.map((item) => [item.cciId, item]), + ); + + const cciMap = new Map(); + + for (const item of cciItems) { + for (const reference of item.CciReferences ?? []) { + if (!reference.index) continue; + + const controlNumber = parseTopLevelControl(reference.index); + const mappedCcis = cciMap.get(controlNumber) ?? []; + + if (!mappedCcis.includes(item.cciId)) { + mappedCcis.push(item.cciId); + } + + cciMap.set(controlNumber, mappedCcis); + } + } + + return { + cciItemMap, + cciMap, + }; +} + export async function getControlSummary( BoundaryId: number, ControlRecordId: number, -): Promise { +): Promise<(ControlSummary | EnhancementSummary)[]> { const perfTimer = new PerfTimer(); perfTimer.start("Query"); const results = await ControlRecordItem.findAll({ + where: { ControlRecordId }, include: [ { model: ControlRecord, attributes: ["ControlFamilyId"], - where: { BoundaryId, id: ControlRecordId }, + where: { BoundaryId }, required: true, include: [{ model: ControlFamily, attributes: ["name"] }], }, @@ -200,110 +526,121 @@ export async function getControlSummary( ], ], }); - perfTimer.stop("Query"); - const controlSummaries: ControlSummary[] = results.map((item) => { - if (item.ControlEnhancementId && item.ControlEnhancement) { - return { - type: "enhancement", - ControlRecordItemId: item.id, - id: item.ControlEnhancement.id, - family: item.ControlRecord?.ControlFamily?.name || "", - number: item.ControlEnhancement.enhancementIdentifier, - title: item.ControlEnhancement.title, - revision: "", // enhancements may not have a revision - guidance: item.ControlEnhancement.guidance, - statements: item.ControlEnhancement.ControlEnhancementStatements || [], - ...{ - ComplianceStatusId: item.ComplianceStatusId, - ImplementationStatusId: item.ImplementationStatusId, - CommonControlProviderId: item.CommonControlProviderId, - systemProvider: item.systemProvider, - SecurityControlDesignationId: item.SecurityControlDesignationId, - TestMethodId: item.TestMethodId, - naJustification: item.naJustification, - estimatedCompletionDate: item.estimatedCompletionDate, - implementationNarrative: item.implementationNarrative, - responsibleEntities: item.responsibleEntities, - criticality: item.criticality, - FrequencyTypeId: item.FrequencyTypeId, - ConMonMethodId: item.ConMonMethodId, - reporting: item.reporting, - tracking: item.tracking, - conmonComments: item.conmonComments, - SeverityId: item.SeverityId, - RelevanceOfThreatId: item.RelevanceOfThreatId, - LikelihoodId: item.LikelihoodId, - ImpactId: item.ImpactId, - ResidualRiskLevelId: item.ResidualRiskLevelId, - vulnerabilitySummary: item.vulnerabilitySummary, - mitigations: item.mitigations, - impactDescription: item.impactDescription, - recommendations: item.recommendations, - auditor: item.auditor, - AuditControlStatusId: item.AuditControlStatusId, - auditDate: item.auditDate, - auditComments: item.auditComments, - assessor: item.assessor, - AssessorControlStatusId: item.AssessorControlStatusId, - assessorDate: item.assessorDate, - assessorComments: item.assessorComments, - lastUpdate: item.lastUpdate, - creationDate: item.creationDate, - }, - }; - } - if (item.Control) { - return { - type: "control", - ControlRecordItemId: item.id, - id: item.Control.id, - family: item.ControlRecord?.ControlFamily?.name || "", - number: item.Control.ControlNumber?.number || "", - title: item.Control.title, - revision: item.Control.ControlRevision?.name || "", - guidance: item.Control.guidance, - statements: item.Control.ControlStatements || [], - ...{ - ComplianceStatusId: item.ComplianceStatusId, - ImplementationStatusId: item.ImplementationStatusId, - CommonControlProviderId: item.CommonControlProviderId, - systemProvider: item.systemProvider, - SecurityControlDesignationId: item.SecurityControlDesignationId, - TestMethodId: item.TestMethodId, - naJustification: item.naJustification, - estimatedCompletionDate: item.estimatedCompletionDate, - implementationNarrative: item.implementationNarrative, - responsibleEntities: item.responsibleEntities, - criticality: item.criticality, - FrequencyTypeId: item.FrequencyTypeId, - ConMonMethodId: item.ConMonMethodId, - reporting: item.reporting, - tracking: item.tracking, - conmonComments: item.conmonComments, - SeverityId: item.SeverityId, - RelevanceOfThreatId: item.RelevanceOfThreatId, - LikelihoodId: item.LikelihoodId, - ImpactId: item.ImpactId, - ResidualRiskLevelId: item.ResidualRiskLevelId, - vulnerabilitySummary: item.vulnerabilitySummary, - mitigations: item.mitigations, - impactDescription: item.impactDescription, - recommendations: item.recommendations, - auditor: item.auditor, - AuditControlStatusId: item.AuditControlStatusId, - auditDate: item.auditDate, - auditComments: item.auditComments, - assessor: item.assessor, - AssessorControlStatusId: item.AssessorControlStatusId, - assessorDate: item.assessorDate, - assessorComments: item.assessorComments, - lastUpdate: item.lastUpdate, - creationDate: item.creationDate, - }, - }; - } - throw new Error("ControlRecordItem without Control or Enhancement"); + const boundary = await Boundary.findOne({ + where: { id: BoundaryId }, + include: [ + { + model: Classification, + }, + { + model: PolicyDocument, + }, + ], }); + + const cciItems = await loadCciItems( + boundary?.PolicyDocumentId, + ); + + const { cciItemMap, cciMap } = buildCciMaps(cciItems); + + const stigResults = await getEvaluationSummary(BoundaryId, undefined, false); + + const stigArray = Array.isArray(stigResults) ? stigResults : [stigResults]; + const controlToStatusMap = new Map>>(); + const stigLookup: { [vKey: string]: { cciIds: Set; stig: any } } = {}; + + for (const stig of stigArray) { + for (const stigData of stig.StigData) { + for (const stigIdent of stigData.StigIdents) { + const vKey = stigData.vuln_num; + stigLookup[vKey] ??= { cciIds: new Set(), stig }; + stigLookup[vKey].cciIds.add(stigIdent.text); + } + const status = stigData.status; + const vKey = stigData.vuln_num; + const title = stig.title || ""; + const displayValue = title ? `${vKey} - ${title}` : vKey; + + const cciIds = stigData.StigIdents.map((c: any) => c.text); + + for (const cciId of cciIds) { + const cciItem = cciItemMap.get(cciId); + const cciReferences = cciItem?.CciReferences ?? []; + + for (const cciReference of cciReferences) { + if (!cciReference.index) continue; + + const normalizedControl = parseTopLevelControl( + cciReference.index, + ); + + if (!normalizedControl) continue; + + if (!controlToStatusMap.has(normalizedControl)) { + controlToStatusMap.set( + normalizedControl, + new Map>(), + ); + } + + const statusMap = controlToStatusMap.get(normalizedControl)!; + + if (!statusMap.has(status)) { + statusMap.set(status, new Set()); + } + + statusMap.get(status)!.add(displayValue); + } + } + } + } + perfTimer.stop("Query"); + const controlSummaries: (ControlSummary | EnhancementSummary)[] = + results.map((item) => { + if (item.ControlEnhancementId && item.ControlEnhancement) { + const normalizedControlNumber = parseTopLevelControl( + item.ControlEnhancement.enhancementIdentifier, + ); + + const assessment = getTechnicalAssessment( + normalizedControlNumber, + controlToStatusMap, + cciMap, + stigLookup, + ); + + return getEnhancementSummary( + item, + assessment.cci, + assessment.technicalAssessmentStatus, + assessment.technicalAssessmentComments, + ); + } + + if (item.Control?.ControlNumber) { + const normalizedControlNumber = parseTopLevelControl( + item.Control.ControlNumber.number, + ); + + const assessment = getTechnicalAssessment( + normalizedControlNumber, + controlToStatusMap, + cciMap, + stigLookup, + ); + + return getStandardControlSummary( + item, + assessment.cci, + assessment.technicalAssessmentStatus, + assessment.technicalAssessmentComments, + ); + } + + throw new Error("ControlRecordItem without Control or Enhancement"); + }); + return controlSummaries; -} +} \ No newline at end of file