diff --git a/lib/ReviewParser.js b/lib/ReviewParser.js deleted file mode 100644 index 479d2b7..0000000 --- a/lib/ReviewParser.js +++ /dev/null @@ -1,980 +0,0 @@ -(function (exports) { - exports.reviewsFromCkl = function ( - { - data, - fieldSettings, - allowAccept, - importOptions, - valueProcessor, - XMLParser - }) { - - const maxCommentLength = 32767 - - if (!XMLParser) { - if (typeof require === 'function') { - const { requireXMLParser } = require('fast-xml-parser') - XMLParser = requireXMLParser - } - else if (typeof fxp === "object" && typeof fxp.XMLParser === 'function') { - XMLParser = fxp.XMLParser - } - else { - throw(new Error('XMLParser not found')) - } - } - - const normalizeKeys = function (input) { - // lowercase and remove hyphens - if (typeof input !== 'object') return input; - if (Array.isArray(input)) return input.map(normalizeKeys); - return Object.keys(input).reduce(function (newObj, key) { - let val = input[key]; - let newVal = (typeof val === 'object') && val !== null ? normalizeKeys(val) : val; - newObj[key.toLowerCase().replace('-','')] = newVal; - return newObj; - }, {}); - } - const resultMap = { - NotAFinding: 'pass', - Open: 'fail', - Not_Applicable: 'notapplicable', - Not_Reviewed: 'notchecked' - } - const parseOptions = { - allowBooleanAttributes: false, - attributeNamePrefix: "", - cdataPropName: "__cdata", //default is 'false' - ignoreAttributes: false, - parseTagValue: false, - parseAttributeValue: false, - removeNSPrefix: true, - trimValues: true, - tagValueProcessor: valueProcessor, - commentPropName: "__comment", - isArray: (name, jpath, isLeafNode, isAttribute) => { - return name === '__comment' || !isLeafNode - } - } - const parser = new XMLParser(parseOptions) - const parsed = parser.parse(data) - - if (!parsed.CHECKLIST) throw (new Error("No CHECKLIST element")) - if (!parsed.CHECKLIST[0].ASSET) throw (new Error("No ASSET element")) - if (!parsed.CHECKLIST[0].STIGS) throw (new Error("No STIGS element")) - - const comments = parsed['__comment'] - const resultEngineCommon = comments?.length ? processRootXmlComments(comments) : null - - let returnObj = {} - returnObj.target = processAsset(parsed.CHECKLIST[0].ASSET[0]) - if (!returnObj.target.name) { - throw (new Error("No host_name in ASSET")) - } - returnObj.checklists = processIStig(parsed.CHECKLIST[0].STIGS[0].iSTIG) - if (returnObj.checklists.length === 0) { - throw (new Error("STIG_INFO element has no SI_DATA for SID_NAME == stigId")) - } - return (returnObj) - - function processAsset(assetElement) { - let obj = { - name: assetElement.HOST_NAME, - description: null, - ip: assetElement.HOST_IP || null, - fqdn: assetElement.HOST_FQDN || null, - mac: assetElement.HOST_MAC || null, - noncomputing: assetElement.ASSET_TYPE === 'Non-Computing' - } - const metadata = {} - if (assetElement.ROLE) { - metadata.cklRole = assetElement.ROLE - } - if (assetElement.TECH_AREA) { - metadata.cklTechArea = assetElement.TECH_AREA - } - if (assetElement.WEB_OR_DATABASE === 'true') { - metadata.cklWebOrDatabase = 'true' - metadata.cklHostName = assetElement.HOST_NAME - if (assetElement.WEB_DB_SITE) { - metadata.cklWebDbSite = assetElement.WEB_DB_SITE - } - if (assetElement.WEB_DB_INSTANCE) { - metadata.cklWebDbInstance = assetElement.WEB_DB_INSTANCE - } - } - obj.metadata = metadata - return obj - } - - function processIStig(iStigElement) { - let checklistArray = [] - iStigElement.forEach(iStig => { - let checklist = {} - // get benchmarkId - let stigIdElement = iStig.STIG_INFO[0].SI_DATA.filter( d => d.SID_NAME === 'stigid' )?.[0] - checklist.benchmarkId = stigIdElement.SID_DATA.replace('xccdf_mil.disa.stig_benchmark_', '') - // get revision data. Extract digits from version and release fields to create revisionStr, if possible. - const stigVersionData = iStig.STIG_INFO[0].SI_DATA.filter( d => d.SID_NAME === 'version' )?.[0].SID_DATA - let stigVersion = stigVersionData.match(/(\d+)/)?.[1] - let stigReleaseInfo = iStig.STIG_INFO[0].SI_DATA.filter( d => d.SID_NAME === 'releaseinfo' )?.[0].SID_DATA - const stigRelease = stigReleaseInfo.match(/Release:\s*(.+?)\s/)?.[1] - const stigRevisionStr = stigVersion && stigRelease ? `V${stigVersion}R${stigRelease}` : null - checklist.revisionStr = stigRevisionStr - - if (checklist.benchmarkId) { - let x = processVuln(iStig.VULN) - checklist.reviews = x.reviews - checklist.stats = x.stats - checklistArray.push(checklist) - } - }) - return checklistArray - } - - function processVuln(vulnElements) { - // vulnElements is an array of this object: - // { - // COMMENTS - // FINDING_DETAILS - // SEVERITY_JUSTIFICATION - // SEVERITY_OVERRIDE - // STATUS - // STIG_DATA [26] - // } - - let vulnArray = [] - let resultStats = { - pass: 0, - fail: 0, - notapplicable: 0, - notchecked: 0, - notselected: 0, - informational: 0, - error: 0, - fixed: 0, - unknown: 0 - } - vulnElements?.forEach(vuln => { - const review = generateReview(vuln, resultEngineCommon) - if (review) { - vulnArray.push(review) - resultStats[review.result]++ - } - }) - - return { - reviews: vulnArray, - stats: resultStats - } - } - - function generateReview(vuln, resultEngineCommon) { - let result = resultMap[vuln.STATUS] - if (!result) return - const ruleId = getRuleIdFromVuln(vuln) - if (!ruleId) return - - const hasComments = !!vuln.FINDING_DETAILS || !!vuln.COMMENTS - - if (result === 'notchecked') { // unreviewed business rules - switch (importOptions.unreviewed) { - case 'never': - return undefined - case 'commented': - result = hasComments ? importOptions.unreviewedCommented : undefined - if (!result) return - break - case 'always': - result = hasComments ? importOptions.unreviewedCommented : 'notchecked' - break - } - } - - let detail = vuln.FINDING_DETAILS.length > maxCommentLength ? vuln.FINDING_DETAILS.slice(0, maxCommentLength) : vuln.FINDING_DETAILS - if (!vuln.FINDING_DETAILS) { - switch (importOptions.emptyDetail) { - case 'ignore': - detail= null - break - case 'import': - detail = vuln.FINDING_DETAILS - break - case 'replace': - detail = 'There is no detail provided for the assessment' - break - } - } - - let comment = vuln.COMMENTS.length > maxCommentLength ? vuln.COMMENTS.slice(0, maxCommentLength) : vuln.COMMENTS - if (!vuln.COMMENTS) { - switch (importOptions.emptyComment) { - case 'ignore': - comment = null - break - case 'import': - comment = vuln.COMMENTS - break - case 'replace': - comment = 'There is no comment provided for the assessment' - break - } - } - - const review = { - ruleId, - result, - detail, - comment - } - - if (resultEngineCommon) { - review.resultEngine = {...resultEngineCommon} - if (vuln['__comment']) { - const overrides = [] - for (const comment of vuln['__comment']) { - if (comment.toString().startsWith('')) { - let override - try { - override = parser.parse(comment)['Evaluate-STIG'][0] - } - catch(e) { - console.log(`Failed to parse Evaluate-STIG VULN XML comment for ${ruleId}`) - } - override = normalizeKeys(override) - if (override.afmod?.toLowerCase() === 'true') { - overrides.push({ - authority: override.answerfile, - oldResult: resultMap[override.oldstatus] ?? 'unknown', - newResult: result, - remark: 'Evaluate-STIG Answer File' - }) - } - } - } - if (overrides.length) { - review.resultEngine.overrides = overrides - } - } - } - else { - review.resultEngine = null - } - - const status = bestStatusForReview(review) - if (status) { - review.status = status - } - - return review - } - - function getRuleIdFromVuln(vuln) { - let ruleId - vuln.STIG_DATA.some(stigDatum => { - if (stigDatum.VULN_ATTRIBUTE == "Rule_ID") { - ruleId = stigDatum.ATTRIBUTE_DATA - return true - } - }) - return ruleId - } - - function bestStatusForReview(review) { - if (importOptions.autoStatus === 'null') return null - if (importOptions.autoStatus === 'saved') return 'saved' - - let detailSubmittable = false - switch (fieldSettings.detail.required) { - case 'optional': - detailSubmittable = true - break - case 'findings': - if ((review.result !== 'fail') || (review.result === 'fail' && review.detail)) { - detailSubmittable = true - } - break - case 'always': - if (review.detail) { - detailSubmittable = true - } - break - } - - let commentSubmittable = false - switch (fieldSettings.comment.required) { - case 'optional': - commentSubmittable = true - break - case 'findings': - if ((review.result !== 'fail') || (review.result === 'fail' && review.comment)) { - commentSubmittable = true - } - break - case 'always': - if (review.comment) { - commentSubmittable = true - } - break - } - - const resultSubmittable = review.result === 'pass' || review.result === 'fail' || review.result === 'notapplicable' - - let status = undefined - if (detailSubmittable && commentSubmittable && resultSubmittable) { - switch (importOptions.autoStatus) { - case 'submitted': - status = 'submitted' - break - case 'accepted': - status = allowAccept ? 'accepted' : 'submitted' - break - } - } - else { - status = 'saved' - } - return status - } - - function processRootXmlComments(comments) { - let resultEngineRoot - for (const comment of comments) { - if (comment.toString().startsWith('')) { - let esRootComment - try { - esRootComment = parser.parse(comment)['Evaluate-STIG'][0] - } - catch(e) { - console.log('Failed to parse Evaluate-STIG root XML comment') - } - esRootComment = normalizeKeys(esRootComment) - resultEngineRoot = { - type: 'script', - product: 'Evaluate-STIG', - version: esRootComment?.global?.[0]?.version, - time: esRootComment?.global?.[0]?.time, - checkContent: { - location: esRootComment?.module?.[0]?.name ?? '' - } - } - } - } - return resultEngineRoot || null - } - } - - exports.reviewsFromXccdf = function ( - { - data, - fieldSettings, - allowAccept, - importOptions, - valueProcessor, - scapBenchmarkMap, - XMLParser - }) { - - // Parse the XML - const parseOptions = { - allowBooleanAttributes: false, - attributeNamePrefix: "", - cdataPropName: "__cdata", //default is 'false' - ignoreAttributes: false, - parseTagValue: false, - removeNSPrefix: true, - trimValues: true, - tagValueProcessor: valueProcessor, - commentPropName: "__comment", - isArray: (name, jpath, isLeafNode, isAttribute) => { - const arrayElements = [ - 'override', - 'overrides', - 'target', - 'target-address', - 'fact' - ] - return arrayElements.includes(name) - } - } - const parser = new XMLParser(parseOptions) - let parsed = parser.parse(data) - - // Basic sanity checks, handle root element with child - let benchmarkId, testResult - if (!parsed.Benchmark && !parsed.TestResult) throw (new Error("No Benchmark or TestResult element")) - if (parsed.Benchmark) { - if (!parsed.Benchmark.TestResult) throw (new Error("No Benchmark.TestResult element")) - if (!parsed.Benchmark.TestResult['target']) throw (new Error("No Benchmark.TestResult.target element")) - if (!parsed.Benchmark.TestResult['rule-result']) throw (new Error("No Benchmark.TestResult.rule-result element")) - testResult = parsed.Benchmark.TestResult - benchmarkId = parsed.Benchmark.id.replace('xccdf_mil.disa.stig_benchmark_', '') - } - else { - if (!parsed.TestResult['benchmark']) throw (new Error("No TestResult.benchmark element")) - if (!parsed.TestResult['target']) throw (new Error("No TestResult.target element")) - if (!parsed.TestResult['rule-result']) throw (new Error("No TestResult.rule-result element")) - testResult = parsed.TestResult - let benchmarkAttr - if (testResult.benchmark.id?.startsWith('xccdf_mil.disa.stig_benchmark_')) { - benchmarkAttr = testResult.benchmark.id - } - else if (testResult.benchmark.href?.startsWith('xccdf_mil.disa.stig_benchmark_')){ - benchmarkAttr = testResult.benchmark.href - } - else { - throw (new Error("TestResult.benchmark has no attribute starting with xccdf_mil.disa.stig_benchmark_")) - } - benchmarkId = benchmarkAttr.replace('xccdf_mil.disa.stig_benchmark_', '') - } - let DEFAULT_RESULT_TIME = testResult['end-time'] //required by XCCDF 1.2 rev 4 spec - - // Process parsed data - if (scapBenchmarkMap && scapBenchmarkMap.has(benchmarkId)) { - benchmarkId = scapBenchmarkMap.get(benchmarkId) - } - const target = processTarget(testResult) - if (!target.name) { - throw (new Error('No value for ')) - } - - // resultEngine info - const testSystem = testResult['test-system'] - // SCC injects a CPE WFN bound to a URN - const m = testSystem.match(/[c][pP][eE]:\/[AHOaho]?:(.*)/) - let vendor, product, version - if (m?.[1]) { - ;[vendor, product, version] = m[1].split(':') - } - else { - ;[product, version] = testSystem.split(':') // e.g. PAAuditEngine:6.5.3 - } - const resultEngineTpl = { - type: 'scap', - product, - version - } - const r = processRuleResults(testResult['rule-result'], resultEngineTpl) - - // Return object - return ({ - target, - checklists: [{ - benchmarkId: benchmarkId, - revisionStr: null, - reviews: r.reviews, - stats: r.stats - }] - }) - - function processRuleResults(ruleResults, resultEngineTpl) { - const stats = { - pass: 0, - fail: 0, - notapplicable: 0, - notchecked: 0, - notselected: 0, - informational: 0, - error: 0, - fixed: 0, - unknown: 0 - } - const reviews = [] - for (const ruleResult of ruleResults) { - const review = generateReview(ruleResult, resultEngineTpl) - if (review) { - reviews.push(review) - stats[review.result]++ - } - } - return { reviews, stats } - } - - function generateReview(ruleResult, resultEngineCommon) { - let result = ruleResult.result - if (!result) return - const ruleId = ruleResult.idref.replace('xccdf_mil.disa.stig_rule_', '') - if (!ruleId) return - - const hasComments = false // or look for - - if (result !== 'pass' && result !== 'fail' && result !== 'notapplicable') { // unreviewed business rules - switch (importOptions.unreviewed) { - case 'never': - return undefined - case 'commented': - result = hasComments ? importOptions.unreviewedCommented : undefined - if (!result) return - break - case 'always': - result = hasComments ? importOptions.unreviewedCommented : 'notchecked' - break - } - } - - let resultEngine - if (resultEngineCommon) { - if (resultEngineCommon.product === 'stig-manager') { - resultEngine = ruleResult.check?.['check-content']?.resultEngine - } - else { - // build the resultEngine value - const timeStr = ruleResult.time ?? DEFAULT_RESULT_TIME - resultEngine = { - time: (timeStr ? new Date(timeStr) : new Date()).toISOString(), - ...resultEngineCommon - } - // handle check-content-ref, if it exists - const checkContentHref = ruleResult?.check?.['check-content-ref']?.href?.replace('#scap_mil.disa.stig_comp_','') - const checkContentName = ruleResult?.check?.['check-content-ref']?.name?.replace('oval:mil.disa.stig.','') - if (checkContentHref || checkContentName) { - resultEngine.checkContent = { - location: checkContentHref, - component: checkContentName - } - } - - if (ruleResult.override?.length) { //overrides - const overrides = [] - for (const override of ruleResult.override) { - overrides.push({ - authority: override.authority, - oldResult: override['old-result'], - newResult: override['new-result'], - remark: override['remark'] - }) - } - if (overrides.length) { - resultEngine.overrides = overrides - } - } - } - } - - const replacementText = `Result was reported by product "${resultEngine?.product}" version ${resultEngine?.version} at ${resultEngine?.time} using check content "${resultEngine?.checkContent?.location}"` - - let detail = ruleResult.check?.['check-content']?.detail - if (!detail) { - switch (importOptions.emptyDetail) { - case 'ignore': - detail= null - break - case 'import': - detail = '' - break - case 'replace': - detail = replacementText - break - } - } - - let comment = ruleResult.check?.['check-content']?.comment - if (!comment) { - switch (importOptions.emptyComment) { - case 'ignore': - comment = null - break - case 'import': - comment = '' - break - case 'replace': - comment = replacementText - break - } - } - - const review = { - ruleId, - result, - resultEngine, - detail, - comment - } - - const status = bestStatusForReview(review) - if (status) { - review.status = status - } - - return review - } - - function bestStatusForReview(review) { - if (importOptions.autoStatus === 'null') return undefined - if (importOptions.autoStatus === 'saved') return 'saved' - - const fields = ['detail', 'comment'] - let commentsSubmittable - for (const field of fields) { - switch (fieldSettings[field].required) { - case 'optional': - commentsSubmittable = true - break - case 'findings': - commentsSubmittable = ((review.result !== 'fail') || (review.result === 'fail' && review[field])) - break - case 'always': - commentsSubmittable = !!review[field] - break - } - if (!commentsSubmittable) break // can end loop if commentsSubmittable becomes false - } - - const resultSubmittable = review.result === 'pass' || review.result === 'fail' || review.result === 'notapplicable' - - let status = undefined - if (commentsSubmittable && resultSubmittable) { - switch (importOptions.autoStatus) { - case 'submitted': - status = 'submitted' - break - case 'accepted': - status = allowAccept ? 'accepted' : 'submitted' - break - } - } - else { - status = 'saved' - } - return status - } - - function processTargetFacts(targetFacts) { - if (!targetFacts) return {} - - const asset = { metadata: {} } - const reTagAsset = /^tag:stig-manager@users.noreply.github.com,2020:asset:(.*)/ - const reMetadata = /^metadata:(.*)/ - - for (const targetFact of targetFacts) { - const matchesTagAsset = targetFact['name'].match(reTagAsset) - if (!matchesTagAsset) { - asset.metadata[targetFact['name']] = targetFact['#text'] - continue - } - const property = matchesTagAsset[1] - const matchesMetadata = property.match(reMetadata) - if (matchesMetadata) { - asset.metadata[decodeURI(matchesMetadata[1])] = targetFact['#text'] - } - else { - let value = targetFact['#text'] - if (property === 'noncomputing') { - value = value === 'true' - } - if (['name','description','fqdn','ip','mac','noncomputing'].includes(property)) { - asset[property] = value - } - } - } - return asset - } - - function processTarget(testResult) { - const assetFromFacts = processTargetFacts(testResult['target-facts']?.fact) - return { - name: testResult.target[0], - description: '', - ip: testResult['target-address']?.[0] || '', - noncomputing: false, - metadata: {}, - ...assetFromFacts - } - } - } - - exports.reviewsFromCklb = function ( - { - data, - fieldSettings, - allowAccept, - importOptions - }) { - - const maxCommentLength = 32767 - const resultMap = { - not_a_finding: 'pass', - open: 'fail', - not_applicable: 'notapplicable', - not_reviewed: 'notchecked' - } - let cklb - try { - cklb = JSON.parse(data) - } - catch (e) { - throw(new Error('Cannot parse as JSON')) - } - const validateCklb = (obj) => { - try { - if (!obj.target_data?.host_name) { - throw('No target_data.host_name found') - } - if (!Array.isArray(obj.stigs)) { - throw('No stigs array found') - } - return {valid: true} - } - catch (e) { - let error = e - if (e instanceof Error) { - error = e.message - } - return {valid: false, error} - } - } - - const validationResult = validateCklb(cklb) - if (!validationResult.valid) { - throw(new Error(`Invalid CKLB object: ${validationResult.error}`)) - } - - const resultEngineCommon = cklb.stig_manager_engine || null - let returnObj = {} - returnObj.target = processTargetData(cklb.target_data) - if (!returnObj.target.name) { - throw (new Error("No host_name in target_data")) - } - returnObj.checklists = processStigs(cklb.stigs) - if (returnObj.checklists.length === 0) { - throw (new Error("stigs array is empty")) - } - return (returnObj) - - function processTargetData(td) { - const obj = { - name: td.host_name, - description: td.comments, - ip: td.ip_address || null, - fqdn: td.fqdn || null, - mac: td.mac_address || null, - noncomputing: td.target_type === 'Non-Computing', - metadata: {} - } - if (td.role) { - obj.metadata.cklRole = td.ROLE - } - if (td.technology_area) { - obj.metadata.cklTechArea = td.technology_area - } - if (td.is_web_database) { - obj.metadata.cklWebOrDatabase = 'true' - obj.metadata.cklHostName = td.host_name - if (td.web_db_site) { - obj.metadata.cklWebDbSite = td.web_db_site - } - if (td.web_db_instance) { - obj.metadata.cklWebDbInstance = td.web_db_instance - } - } - return obj - } - function processStigs(stigs) { - const checklistArray = [] - for (const stig of stigs) { - // checklist = { - // benchmarkId: 'string', - // revisionStr: 'string', - // reviews: [], - // stats: {} - // } - const checklist = {} - checklist.benchmarkId = typeof stig?.stig_id === 'string' ? stig.stig_id.replace('xccdf_mil.disa.stig_benchmark_', '') : '' - const stigVersion = '0' - const stigRelease = typeof stig?.release_info === 'string' ? stig.release_info.match(/Release:\s*(.+?)\s/)?.[1] : '' - checklist.revisionStr = checklist.benchmarkId && stigRelease ? `V${stigVersion}R${stigRelease}` : null - - if (checklist.benchmarkId) { - const result = processRules(stig.rules) - checklist.reviews = result.reviews - checklist.stats = result.stats - checklistArray.push(checklist) - } - - } - return checklistArray - } - function processRules(rules) { - const stats = { - pass: 0, - fail: 0, - notapplicable: 0, - notchecked: 0, - notselected: 0, - informational: 0, - error: 0, - fixed: 0, - unknown: 0 - } - const reviews = [] - for (const rule of rules) { - const review = generateReview(rule, resultEngineCommon) - if (review) { - reviews.push(review) - stats[review.result]++ - } - } - return { reviews, stats } - } - function generateReview(rule, resultEngineCommon) { - let result = resultMap[rule.status] - if (!result) return - const ruleId = rule.rule_id_src - if (!ruleId) return - - const hasComments = !!rule.finding_details || !!rule.comments - - if (result === 'notchecked') { // unreviewed business rules - switch (importOptions.unreviewed) { - case 'never': - return undefined - case 'commented': - result = hasComments ? importOptions.unreviewedCommented : undefined - if (!result) return - break - case 'always': - result = hasComments ? importOptions.unreviewedCommented : 'notchecked' - break - } - } - - let detail = rule.finding_details?.length > maxCommentLength ? rule.finding_details.slice(0, maxCommentLength) : rule.finding_details - if (!rule.finding_details) { - switch (importOptions.emptyDetail) { - case 'ignore': - detail= null - break - case 'import': - detail = rule.finding_details ?? '' - break - case 'replace': - detail = 'There is no detail provided for the assessment' - break - } - } - - let comment = rule.comments?.length > maxCommentLength ? rule.comments.slice(0, maxCommentLength) : rule.comments - if (!rule.comments) { - switch (importOptions.emptyComment) { - case 'ignore': - comment = null - break - case 'import': - comment = rule.comments ?? '' - break - case 'replace': - comment = 'There is no comment provided for the assessment' - break - } - } - - const review = { - ruleId, - result, - detail, - comment - } - - // if (resultEngineCommon) { - // review.resultEngine = {...resultEngineCommon} - // if (rule.stig_manager_engine) { - // const overrides = [] - // for (const comment of vuln['__comment']) { - // if (comment.toString().startsWith('')) { - // let override - // try { - // override = parser.parse(comment)['Evaluate-STIG'][0] - // } - // catch(e) { - // console.log(`Failed to parse Evaluate-STIG VULN XML comment for ${ruleId}`) - // } - // override = normalizeKeys(override) - // if (override.afmod?.toLowerCase() === 'true') { - // overrides.push({ - // authority: override.answerfile, - // oldResult: resultMap[override.oldstatus] ?? 'unknown', - // newResult: result, - // remark: 'Evaluate-STIG Answer File' - // }) - // } - // } - // } - // if (overrides.length) { - // review.resultEngine.overrides = overrides - // } - // } - // } - // else { - // review.resultEngine = null - // } - - const status = bestStatusForReview(review) - if (status) { - review.status = status - } - - return review - } - function bestStatusForReview(review) { - if (importOptions.autoStatus === 'null') return null - if (importOptions.autoStatus === 'saved') return 'saved' - - let detailSubmittable = false - switch (fieldSettings.detail.required) { - case 'optional': - detailSubmittable = true - break - case 'findings': - if ((review.result !== 'fail') || (review.result === 'fail' && review.detail)) { - detailSubmittable = true - } - break - case 'always': - if (review.detail) { - detailSubmittable = true - } - break - } - - let commentSubmittable = false - switch (fieldSettings.comment.required) { - case 'optional': - commentSubmittable = true - break - case 'findings': - if ((review.result !== 'fail') || (review.result === 'fail' && review.comment)) { - commentSubmittable = true - } - break - case 'always': - if (review.comment) { - commentSubmittable = true - } - break - } - - const resultSubmittable = review.result === 'pass' || review.result === 'fail' || review.result === 'notapplicable' - - let status = undefined - if (detailSubmittable && commentSubmittable && resultSubmittable) { - switch (importOptions.autoStatus) { - case 'submitted': - status = 'submitted' - break - case 'accepted': - status = allowAccept ? 'accepted' : 'submitted' - break - } - } - else { - status = 'saved' - } - return status - } - - - } - - exports.reviewsFromScc = exports.reviewsFromXccdf - -}) (typeof exports === 'undefined'? this['ReviewParser'] = {} : exports) \ No newline at end of file diff --git a/lib/cargo.js b/lib/cargo.js index a637cb9..0367877 100644 --- a/lib/cargo.js +++ b/lib/cargo.js @@ -3,235 +3,11 @@ const { logger, getSymbol } = require('./logger') const Queue = require('better-queue') const api = require('./api') const { serializeError } = require('serialize-error') +const {TaskObject} = require('stig-manager-client-modules') const component = 'cargo' let batchId = 0 -class TaskObject { - constructor({ apiAssets = [], apiStigs = [], parsedResults = [] }) { - // An array of results from the parsers - this.parsedResults = parsedResults - - // An array of assets from the API - this.apiAssets = apiAssets - // Create Maps of the assets by assetName and metadata.cklHostName - this.mappedAssetNames = new Map() - this.mappedCklHostnames = new Map() - for (const apiAsset of apiAssets) { - // Update .stigs to an array of benchmarkId strings - apiAsset.stigs = apiAsset.stigs.map(stig => stig.benchmarkId) - this.mappedAssetNames.set(apiAsset.name.toLowerCase(), apiAsset) - if (apiAsset.metadata?.cklHostName) { - const v = this.mappedCklHostnames.get(apiAsset.metadata.cklHostName.toLowerCase()) - if (v) { - v.push(apiAsset) - } - else { - this.mappedCklHostnames.set(apiAsset.metadata.cklHostName.toLowerCase(), [apiAsset]) - } - } - } - - // A Map() of the installed benchmarkIds return by the API - // key: benchmarkId, value: array of revisionStr - this.mappedStigs = new Map() - for (const apiStig of apiStigs) { - this.mappedStigs.set(apiStig.benchmarkId, apiStig.revisionStrs) - } - - // An array of accumulated errors - this.errors = [] - - // A Map() of assets to be processed by the writer - this.taskAssets = this._createTaskAssets() - } - - _findAssetFromParsedTarget(target) { - if (!target.metadata.cklHostName) { - return this.mappedAssetNames.get(target.name.toLowerCase()) - } - const matchedByCklHostname = this.mappedCklHostnames.get(target.metadata.cklHostName.toLowerCase()) - if (!matchedByCklHostname) return null - const matchedByAllCklMetadata = matchedByCklHostname.find( - asset => asset.metadata.cklWebDbInstance?.toLowerCase() === target.metadata.cklWebDbInstance?.toLowerCase() - && asset.metadata.cklWebDbSite?.toLowerCase() === target.metadata.cklWebDbSite?.toLowerCase()) - if (!matchedByAllCklMetadata) return null - return matchedByAllCklMetadata - } - - _createTaskAssets() { - // taskAssets is a Map() keyed by lowercase asset name (or CKL metadata), the value is an object: - // { - // knownAsset: false, // does the asset need to be created - // assetProps: null, // an Asset object suitable for put/post to the API - // hasNewAssignment: false, // are there new STIG assignments? - // newAssignments: [], // any new assignments - // checklists: new Map(), // the vetted result checklists, a Map() keyed by benchmarkId - // checklistsIgnored: [], // the ignored checklists - // reviews: [] // the vetted reviews - // } - - - const taskAssets = new Map() - - // if parsedResults is an object convert it to array (cargosize=1) - this.parsedResults = Array.isArray(this.parsedResults) ? this.parsedResults : [this.parsedResults] - - - for (const parsedResult of this.parsedResults) { - // Generate mapping key - let mapKey, tMeta = parsedResult.target.metadata - if (!tMeta.cklHostName) { - mapKey = parsedResult.target.name.toLowerCase() - } - else { - mapKey = `${tMeta.cklHostName}-${tMeta.cklWebDbSite ?? 'NA'}-${tMeta.cklWebDbInstance ?? 'NA'}` - } - - // Try to find the asset in the API response - const apiAsset = this._findAssetFromParsedTarget(parsedResult.target) - if (!apiAsset && !config.createObjects) { - // Bail if the asset doesn't exist and we won't create it - this.errors.push({ - file: parsedResult.file, - message: `asset does not exist for target`, - target: parsedResult.target - }) - logger.warn({ - component: 'taskbuilder', - message: 'target ignored', - reason: 'asset does not exist for target and options.createObjects == false', - file: parsedResult.file, - target: parsedResult.target - }) - continue - } - // Try to find the target in our Map() - let taskAsset = taskAssets.get(mapKey) - - if (!taskAsset) { - // This is our first encounter with this assetName, initialize Map() value - taskAsset = { - knownAsset: false, - assetProps: null, // an object suitable for put/post to the API - hasNewAssignment: false, - newAssignments: [], - checklists: new Map(), // the vetted result checklists - checklistsIgnored: [], // the ignored checklists - reviews: [] // the vetted reviews - } - if (!apiAsset) { - // The asset does not exist in the API. Set assetProps from this parseResult. - if (!tMeta.cklHostName) { - taskAsset.assetProps = { ...parsedResult.target, collectionId: config.collectionId, stigs: [] } - } - else { - taskAsset.assetProps = { ...parsedResult.target, name: mapKey, collectionId: config.collectionId, stigs: [] } - } - } - else { - // The asset exists in the API. Set assetProps from the apiAsset. - taskAsset.knownAsset = true - taskAsset.assetProps = apiAsset - } - // Insert the asset into taskAssets - taskAssets.set(mapKey, taskAsset) - } - - // Helper functions - const stigIsInstalled = ({ benchmarkId, revisionStr }) => { - const revisionStrs = this.mappedStigs.get(benchmarkId) - if (revisionStrs) { - return revisionStr && config.strictRevisionCheck ? revisionStrs.includes(revisionStr) : true - } - else { - return false - } - } - const stigIsAssigned = ({ benchmarkId }) => { - return taskAsset.assetProps.stigs.includes(benchmarkId) - } - const assignStig = (benchmarkId) => { - if (!stigIsAssigned(benchmarkId)) { - taskAsset.hasNewAssignment = true - taskAsset.newAssignments.push(benchmarkId) - taskAsset.assetProps.stigs.push(benchmarkId) - } - } - const stigIsNewlyAssigned = (benchmarkId) => taskAsset.newAssignments.includes(benchmarkId) - - const addToTaskAssetChecklistMapArray = (taskAsset, checklist) => { - let checklistArray = taskAsset.checklists.get(checklist.benchmarkId) - if (checklistArray) { - checklistArray.push(checklist) - } - else { - taskAsset.checklists.set(checklist.benchmarkId, [checklist]) - } - } - - - // Vet the checklists in this parseResult - for (const checklist of parsedResult.checklists) { - checklist.file = parsedResult.file - if (stigIsInstalled(checklist)) { - if (stigIsAssigned(checklist)) { - checklist.newAssignment = stigIsNewlyAssigned(checklist.benchmarkId) - addToTaskAssetChecklistMapArray(taskAsset, checklist) - logger.debug({ - component: 'taskobject', - message: 'checklist included', - file: parsedResult.file, - assetName: parsedResult.target.name, - benchmarkId: checklist.benchmarkId, - }) - } - else if (config.createObjects) { - assignStig(checklist.benchmarkId) - checklist.newAssignment = true - addToTaskAssetChecklistMapArray(taskAsset, checklist) - logger.debug({ - component: 'taskobject', - message: 'checklist assigned and included', - file: parsedResult.file, - assetName: parsedResult.target.name, - benchmarkId: checklist.benchmarkId, - }) - - } - else { - checklist.ignored = `Not mapped to Asset` - taskAsset.checklistsIgnored.push(checklist) - logger.warn({ - component: 'taskobject', - message: 'checklist ignored', - file: parsedResult.file, - assetName: parsedResult.target.name, - benchmarkId: checklist.benchmarkId, - reason: 'stig is not assigned' - }) - } - } - else { - checklist.ignored = `Not installed` - taskAsset.checklistsIgnored.push(checklist) - logger.warn({ - component: 'taskobject', - message: 'checklist ignored', - file: parsedResult.file, - assetName: parsedResult.target.name, - benchmarkId: checklist.benchmarkId, - reason: 'stig is not installed' - }) - - } - } - - } - return taskAssets - } -} - async function writer ( taskAsset ) { const component = 'writer' try { diff --git a/lib/parse.js b/lib/parse.js index c41ecd0..d24bd65 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1,6 +1,6 @@ const api = require ('./api') const { XMLParser } = require('fast-xml-parser') -const ReviewParser = require('./ReviewParser') +const {reviewsFromCkl, reviewsFromScc, reviewsFromCklb} = require('stig-manager-client-modules') const Queue = require('better-queue') const { logger } = require('./logger') const cargo = require('./cargo') @@ -40,15 +40,15 @@ async function parseFileAndEnqueue (file, cb) { const extension = file.substring(file.lastIndexOf(".") + 1) let parseFn, type if (extension.toLowerCase() === 'ckl') { - parseFn = ReviewParser.reviewsFromCkl + parseFn = reviewsFromCkl type = 'CKL' } else if (extension.toLowerCase() === 'xml') { - parseFn = ReviewParser.reviewsFromScc + parseFn = reviewsFromScc type = "XCCDF" } else if (extension.toLowerCase() === 'cklb') { - parseFn = ReviewParser.reviewsFromCklb + parseFn = reviewsFromCklb type = "CKLB" } else { diff --git a/package-lock.json b/package-lock.json index 02add19..8f1a673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "prompt-sync": "4.1.6", "semver": "^7.3.5", "serialize-error": "^8.0.1", + "stig-manager-client-modules": "github:nuwcdivnpt/stig-manager-client-modules#semver:^1.0.0", "winston": "^3.3.3" }, "bin": { @@ -396,17 +397,17 @@ } }, "node_modules/fast-xml-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", - "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.3.tgz", + "integrity": "sha512-coV/D1MhrShMvU6D0I+VAK3umz6hUaxxhL0yp/9RjfiYUfAv14rDhGQL+PLForhMdr0wq3PiV07WtkkNjJjNHg==", "funding": [ - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - }, { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" } ], "dependencies": { @@ -965,6 +966,15 @@ "node": "*" } }, + "node_modules/stig-manager-client-modules": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/nuwcdivnpt/stig-manager-client-modules.git#dc3732b2b7e2360813ce057e34a4de040ee26173", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "fast-xml-parser": "^4.3.2" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1355,9 +1365,9 @@ } }, "fast-xml-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", - "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.3.tgz", + "integrity": "sha512-coV/D1MhrShMvU6D0I+VAK3umz6hUaxxhL0yp/9RjfiYUfAv14rDhGQL+PLForhMdr0wq3PiV07WtkkNjJjNHg==", "requires": { "strnum": "^1.0.5" } @@ -1743,6 +1753,13 @@ "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" }, + "stig-manager-client-modules": { + "version": "git+ssh://git@github.com/nuwcdivnpt/stig-manager-client-modules.git#dc3732b2b7e2360813ce057e34a4de040ee26173", + "from": "stig-manager-client-modules@github:nuwcdivnpt/stig-manager-client-modules#semver:^1.0.0", + "requires": { + "fast-xml-parser": "^4.3.2" + } + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", diff --git a/package.json b/package.json index 99b5e12..6d3ca81 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "prompt-sync": "4.1.6", "semver": "^7.3.5", "serialize-error": "^8.0.1", + "stig-manager-client-modules": "github:nuwcdivnpt/stig-manager-client-modules#semver:^1.0.0", "winston": "^3.3.3" } } diff --git a/pkg.config.json b/pkg.config.json index e268879..f184371 100644 --- a/pkg.config.json +++ b/pkg.config.json @@ -3,6 +3,7 @@ "bin": "./index.js", "pkg": { "targets": [ "node18-win", "node18-linuxstatic" ], + "assets": ["./node_modules/stig-manager-client-modules/index.cjs"], "outputPath": "./bin" } }