Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Checklist metadata validation and checklist mapper severities #2750

Merged
merged 37 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0e7a273
input validation for checklist metadata
kemley76 Jun 6, 2024
0d5fd2c
use hdf-converters in hdf2ckl
kemley76 Jun 7, 2024
f0a2dac
updated hdf2ckl tests
kemley76 Jun 7, 2024
71d53c7
update tests based on changes to ckl mapper
kemley76 Jun 28, 2024
f1d09e3
update ckl metadata validation to use hdf-converters helper function
kemley76 Jul 1, 2024
dd3fbd3
added ability to use local install of inspecjs
kemley76 Jul 9, 2024
1999bd5
update checklist commands and tests
kemley76 Jul 9, 2024
0ff6be4
ensure threshold counts stay based off impact
kemley76 Jul 9, 2024
69e94c0
added tests to ensure that converting with invalid metadata display a…
kemley76 Jul 10, 2024
5d2ffda
use checklist types from hdf-converters
kemley76 Jul 10, 2024
16e1bc6
remove redundant code in hdf2ckl command
kemley76 Jul 10, 2024
4cfe734
use inspecJS to convert impact to severity
kemley76 Jul 10, 2024
4482231
use checklist types from hdf-converters
kemley76 Jul 11, 2024
0711ff6
Merge branch 'hdf2ckl-severity-update' into update-hdf-converters
kemley76 Jul 15, 2024
6eaf79e
fix test data
kemley76 Jul 15, 2024
302e731
Merge branch 'main' into update-hdf-converters
kemley76 Jul 15, 2024
1da2b0f
enforce enum matching for user input in generate ckl_metadata command
kemley76 Jul 15, 2024
c4de62d
add backwards compatibility for old checklist metadata format
kemley76 Jul 16, 2024
b3d4724
Merge branch 'main' into update-hdf-converters
kemley76 Jul 23, 2024
72c8f39
remove debug statement
kemley76 Jul 23, 2024
02b21d2
fix code smells
kemley76 Jul 23, 2024
11991ca
linting
kemley76 Jul 23, 2024
5a091f4
format every output json file with 2 space indent
kemley76 Jul 23, 2024
e540f79
add flags for all metadata fields on hdf2ckl command
kemley76 Jul 24, 2024
c531d2b
clarify instructions on ckl metadata generation
kemley76 Jul 24, 2024
83c98f1
change formating from 4 to 2 space indent
kemley76 Jul 24, 2024
14aa7be
make version and release number optional in checklist metadata genera…
kemley76 Jul 24, 2024
9500d89
update tests to reflect better formatted error messages
kemley76 Jul 24, 2024
a84c21a
update markdown summary table to include row for severity: none
kemley76 Jul 25, 2024
4de13d1
update code and tests to count N/A controls with severity other than …
kemley76 Jul 25, 2024
81a36bb
Merge branch 'main' into update-hdf-converters
kemley76 Jul 25, 2024
b4fa9f6
fix code smells
kemley76 Jul 26, 2024
7ad5e57
revert addition of severity-none row to markdown summary table
kemley76 Jul 29, 2024
be94295
Merge branch 'main' into update-hdf-converters
Amndeep7 Jul 31, 2024
61e1dff
remove heimdall version when running checklist tests
kemley76 Jul 31, 2024
a6b99b5
change return type of string | undefined to string | null
kemley76 Jul 31, 2024
2f5f496
refactor to avoid while true loops
kemley76 Jul 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions pack-inspecjs.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
ECHO OFF

SET CYPRESS_INSTALL_BINARY=0
SET PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true

SET original_dir=%cd%
ECHO %original_dir%

IF DEFINED npm_config_heimdall (
CD %npm_config_heimdall%/libs/inspecjs/
) ELSE (
CD ../heimdall2/libs/inspecjs/
)

IF DEFINED npm_config_branch (
CALL git switch %npm_config_branch% || EXIT /B %ERRORLEVEL%
) ELSE (
CALL git switch master || EXIT /B %ERRORLEVEL%
)

ECHO Executing - git fetch ...
CALL git fetch || EXIT /B %ERRORLEVEL%

ECHO Executing - git pull ...
CALL git pull || EXIT /B %ERRORLEVEL%

ECHO Executing - yarn install ...
CALL yarn install || EXIT /B %ERRORLEVEL%

ECHO Executing - yarn pack ...
CALL yarn pack || EXIT /B %ERRORLEVEL%

ECHO Finished generating the tarball

CD %original_dir%

ECHO Executing - npm install remote ...
CALL npm i || EXIT /B %ERRORLEVEL%

ECHO Executing - npm install local ...

IF DEFINED npm_config_heimdall (
FOR /f "tokens=*" %%a IN ('dir /b %npm_config_heimdall%\libs\inspecjs\inspecjs-v*.tgz') DO (
SET THIS_TAR_ZIP=%npm_config_heimdall%\libs\inspecjs\%%a
)
) ELSE (
FOR /f "tokens=*" %%a IN ('dir /b ..\heimdall2\libs\inspecjs\inspecjs-v*.tgz') DO (
SET THIS_TAR_ZIP=..\heimdall2\libs\inspecjs\%%a
)
)
CALL npm i %THIS_TAR_ZIP% || EXIT /B %ERRORLEVEL%

ECHO Executing - npm run prepack ...
CALL npm run prepack || EXIT /B %ERRORLEVEL%

ECHO Install of local inspecjs complete.
40 changes: 40 additions & 0 deletions pack-inspecjs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash

set -o errexit # abort on nonzero exitstatus
set -o nounset # abort on unbound variable
set -o pipefail # don't hide errors within pipes

ORIGINAL=$PWD
echo $ORIGINAL

cd "${npm_config_heimdall:-../heimdall2}"
cd libs/inspecjs

git switch "${npm_config_branch:-master}"

echo "Executing - git fetch ..."
git fetch

echo "Executing - git pull ..."
git pull

echo "Executing - yarn install ..."
CYPRESS_INSTALL_BINARY=0 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true yarn install

echo "Executing - yarn pack ..."
yarn pack

echo "Finished generating the tarball"

cd "$ORIGINAL"

echo "Executing - npm install remote ..."
npm i

echo "Executing - npm install local ..."
npm i "${npm_config_heimdall:-../heimdall2}/libs/inspecjs/inspecjs-v"*".tgz"

echo "Executing - npm run prepack ..."
npm run prepack

echo "Install of local inspecjs complete."
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,10 @@
"prepack:darwin:linux": "rm -rf lib && tsc",
"pack-hdf-converters": "run-script-os",
"pack-hdf-converters:win32": "pack-hdf-converters.bat",
"pack-hdf-converters:darwin:linux": "./pack-hdf-converters.sh"
"pack-hdf-converters:darwin:linux": "./pack-hdf-converters.sh",
"pack-inspecjs": "run-script-os",
"pack-inspecjs:win32": "pack-inspecjs.bat",
"pack-inspecjs:darwin:linux": "./pack-inspecjs.sh"
},
"types": "lib/index.d.ts",
"jest": {
Expand Down
8 changes: 6 additions & 2 deletions src/commands/convert/ckl2hdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ export default class CKL2HDF extends Command {
const data = fs.readFileSync(flags.input, 'utf8')
checkInput({data, filename: flags.input}, 'checklist', 'DISA Checklist')

const converter = new Mapper(data, flags['with-raw'])
fs.writeFileSync(checkSuffix(flags.output), JSON.stringify(converter.toHdf()))
try {
const converter = new Mapper(data, flags['with-raw'])
fs.writeFileSync(checkSuffix(flags.output), JSON.stringify(converter.toHdf()))
} catch (error) {
console.error(`Error converting to hdf:\n${error}`)
}
}
}
109 changes: 63 additions & 46 deletions src/commands/convert/hdf2ckl.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import {Command, Flags} from '@oclif/core'
import {contextualizeEvaluation} from 'inspecjs'
import _ from 'lodash'
import fs from 'fs'
import {v4} from 'uuid'
import {default as files} from '../../resources/files.json'
import Mustache from 'mustache'
import {CKLMetadata} from '../../types/checklist'
import {convertFullPathToFilename, getProfileInfo} from '../../utils/global'
import {getDetails} from '../../utils/checklist'
import {Assettype, ChecklistMetadata, ChecklistResults as Mapper, Role, Techarea, validateChecklistMetadata} from '@mitre/hdf-converters'

Check failure on line 4 in src/commands/convert/hdf2ckl.ts

View workflow job for this annotation

GitHub Actions / build

'"@mitre/hdf-converters"' has no exported member named 'validateChecklistMetadata'. Did you mean 'ChecklistMetadata'?

export default class HDF2CKL extends Command {
static usage = 'convert hdf2ckl -i <hdf-scan-results-json> -o <output-ckl> [-h] [-m <metadata>] [-H <hostname>] [-F <fqdn>] [-M <mac-address>] [-I <ip-address>]'
Expand All @@ -25,54 +19,77 @@
ip: Flags.string({char: 'I', required: false, description: 'IP address for CKL metadata'}),
}

static examples = ['saf convert hdf2ckl -i rhel7-results.json -o rhel7.ckl --fqdn reverseproxy.example.org --hostname reverseproxy --ip 10.0.0.3 --mac 12:34:56:78:90']
static readonly examples = ['saf convert hdf2ckl -i rhel7-results.json -o rhel7.ckl --fqdn reverseproxy.example.org --hostname reverseproxy --ip 10.0.0.3 --mac 12:34:56:78:90:AB']

static readonly oldMetadataFormatMapping = {
'profiles[0].name': 'benchmark.title',
'profiles[0].title': 'benchmark.title',
stigguid: 'stigid',
role: 'role',
assettype: 'type',
hostname: 'hostname',
hostip: 'ip',
hostmac: 'mac',
techarea: 'tech_area',
targetkey: 'target_key',
webdbsite: 'web_db_site',
webdbinstance: 'web_db_site',
}

async run() {
const {flags} = await this.parse(HDF2CKL)
const contextualizedEvaluation = contextualizeEvaluation(JSON.parse(fs.readFileSync(flags.input, 'utf8')))
const profileName = contextualizedEvaluation.data.profiles[0].name
const controls = contextualizedEvaluation.contains.flatMap(profile => profile.contains) || []
const rootControls = _.uniqBy(controls, control =>
_.get(control, 'root.hdf.wraps.id'),
).map(({root}) => root)
let cklData = {}
const cklMetadata: CKLMetadata = {
fileName: convertFullPathToFilename(flags.input),
benchmark: {
title: profileName || null,
version: '1',
plaintext: null,
},
stigid: profileName || null,
role: 'None',
type: 'Computing',
hostname: flags.hostname || _.get(contextualizedEvaluation, 'evaluation.data.passthrough.hostname') || null,
ip: flags.ip || _.get(contextualizedEvaluation, 'evaluation.data.passthrough.ip') || null,
mac: flags.mac || _.get(contextualizedEvaluation, 'evaluation.data.passthrough.mac') || null,
fqdn: flags.fqdn || _.get(contextualizedEvaluation, 'evaluation.data.passthrough.fqdn') || null,
tech_area: null,
target_key: '0',
web_or_database: 'false',
web_db_site: null,
web_db_instance: null,

/* Order of precedence for checklist metadata:
command flags (hostname, ip, etc.)
metadata flag
input hdf file passthrough.metadata
input hdf file passthrough.checklist.asset */

const defaultMetadata: ChecklistMetadata = {
role: Role.None, assettype: Assettype.Computing, webordatabase: 'false', profiles: [],
hostfqdn: '', hostip: '', hostmac: '', marking: '', techarea: Techarea.Empty, vulidmapping: 'id',
hostname: '', targetcomment: '', webdbinstance: '', webdbsite: '',
}
const inputHDF = JSON.parse(fs.readFileSync(flags.input, 'utf8'))
const flagMetadata = {hostname: flags.hostname, hostip: flags.ip, hostmac: flags.mac, hostfqdn: flags.fqdn}
let fileMetadata = flags.metadata ? JSON.parse(fs.readFileSync(flags.metadata, 'utf8')) : {}

// to preserve backwards compatibility with old metadata format
if (flags.metadata && _.has(fileMetadata, 'benchmark')) {
let profile
if (_.has(fileMetadata, 'benchmark.version')) {
const version: string = _.get(fileMetadata, 'benchmark.version')

if (flags.metadata) {
const cklMetadataInput: CKLMetadata = JSON.parse(fs.readFileSync(flags.metadata, 'utf8'))
for (const field in cklMetadataInput) {
if (typeof cklMetadata[field] === 'string' || typeof cklMetadata[field] === 'object') {
cklMetadata[field] = cklMetadataInput[field]
// get sections of numbers in version string
const parsedVersion = version.split(/\D+/)
.filter(Boolean)
.map(s => Number.parseInt(s, 10))
profile = {version: parsedVersion[0], releasenumber: parsedVersion[1]}
} else {
profile = {}
}

const newFileMetadata = {profiles: [profile]}

for (const [newKey, oldKey] of Object.entries(HDF2CKL.oldMetadataFormatMapping)) {
const oldValue = _.get(fileMetadata, oldKey)
if (oldValue) {
_.set(newFileMetadata, newKey, oldValue)
}
}

fileMetadata = newFileMetadata
}

cklData = {
releaseInfo: cklMetadata.benchmark.plaintext,
...cklMetadata,
profileInfo: getProfileInfo(contextualizedEvaluation, cklMetadata.fileName),
uuid: v4(),
controls: rootControls.map(control => getDetails(control, profileName)),
const hdfMetadata = _.get(inputHDF, 'passthrough.metadata', _.get(inputHDF, 'passthrough.checklist.asset', {}))
const metadata = _.merge(defaultMetadata, hdfMetadata, fileMetadata, flagMetadata)
_.set(inputHDF, 'passthrough.metadata', metadata)

const validationResults = validateChecklistMetadata(metadata)
if (validationResults.ok) {
fs.writeFileSync(flags.output, new Mapper(inputHDF).toCkl())
} else {
console.error(`Error creating checklist:\n${validationResults.error.message}`)
}
fs.writeFileSync(flags.output, Mustache.render(files['cklExport.ckl'].data, cklData).replaceAll(/[^\x00-\x7F]/g, ''))
}
}
93 changes: 75 additions & 18 deletions src/commands/generate/ckl_metadata.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,51 @@
import {Command, Flags} from '@oclif/core'
import fs from 'fs'
import promptSync from 'prompt-sync'
import _ from 'lodash'
import {Assettype, ChecklistMetadata, Role, Techarea, validateChecklistMetadata} from '@mitre/hdf-converters'

Check failure on line 4 in src/commands/generate/ckl_metadata.ts

View workflow job for this annotation

GitHub Actions / build

'"@mitre/hdf-converters"' has no exported member named 'validateChecklistMetadata'. Did you mean 'ChecklistMetadata'?
import path from 'path'

const prompt = promptSync()

// Ensures that no empty strings are passed into the metadata
// by returning undefined instead
function enforceNonEmptyString(ask: string) : string | undefined {
const response = prompt({ask})
if (response)
return response
return undefined
}

function enforceInteger(ask: string): number {
let response = prompt({ask})
let intRep: number
while (true) {
intRep = Number.parseInt(response, 10)
const floatRep = Number.parseFloat(response)
if (intRep === floatRep && intRep >= 0 && !Number.isNaN(intRep))
break
console.log(`${response} is not a valid non-negative integer. Please try again`)
response = prompt({ask})
}

return intRep
}

function enforceEnum(ask: string, options: string[]): string | undefined {
// format prompt to show valid options (removes empty string options)
ask = `${ask} (${options.filter(Boolean).join('/')}) `
let response = prompt({ask})
while (true) {
if (options.includes(response))
break
if (!response)
return
console.log(`${response} is not a valid option. Please try again.`)
response = prompt({ask})
}

return response
}

export default class GenerateCKLMetadata extends Command {
static usage = 'generate ckl_metadata -o <json-file> [-h]'

Expand All @@ -21,23 +62,39 @@
const {flags} = await this.parse(GenerateCKLMetadata)
console.log('Please fill in the following fields to the best of your ability, if you do not have a value, please leave the field empty.')
const cklMetadata = {
benchmark: {
title: prompt({ask: 'What is the benchmark title? '}) || null,
version: prompt({ask: 'What is the benchmark version? '}) || null,
plaintext: prompt({ask: 'What is the notes for release info? '}) || null,
},
stigid: prompt({ask: 'What is the STIG ID? '}) || null,
role: prompt({ask: 'What is the computing role? (None/Workstation/Member Server/Domain Controller) '}) || null,
type: _.capitalize(prompt({ask: 'What is the asset type? (Computing/Non-Computing) '})) || null,
hostname: prompt({ask: 'What is the asset hostname? '}) || null,
ip: prompt({ask: 'What is the asset IP address? '}) || null,
mac: prompt({ask: 'What is the asset MAC address? '}) || null,
tech_area: prompt({ask: 'What is the tech area? (Application Review/Boundary Security/CDS Admin Review/CDS Technical Review/Database Review/Domain Name System (DNS)/Exchange Server/Host Based System Security (HBSS)/Internal Network/Mobility/Releasable Networks (REL)/Releaseable Networks (REL)/Traditional Security/UNIX OS/VVOIP Review/Web Review/Windows OS/Other Review) '}) || null, // Yes, these typos really are how the enumerations are defined in STIG viewer's source code
target_key: prompt({ask: 'What is the target key? '}) || null,
web_or_database: prompt({ask: 'Is the target a web or database? (y/n) '}).toLowerCase() === 'y',
web_db_site: prompt({ask: 'What is the Web or DB site? '}) || null,
web_db_instance: prompt({ask: 'What is the Web or DB instance? '}) || null,
profiles: [
{
name: enforceNonEmptyString('What is the benchmark name? (Must match with profile name listed in HDF) '),
title: enforceNonEmptyString('What is the benchmark title? '),
version: enforceInteger('What is the benchmark version? '),
releasenumber: enforceInteger('What is the benchmark release number? '),
releasedate: enforceNonEmptyString('What is the benchmark release date (YYYY/MM/DD)? '),
showCalendar: true,
},
],
marking: enforceNonEmptyString('What is the marking? '),
hostname: enforceNonEmptyString('What is the asset hostname? '),
hostip: enforceNonEmptyString('What is the asset IP address? '),
hostmac: enforceNonEmptyString('What is the asset MAC address? '),
hostfqdn: enforceNonEmptyString('What is the asset FQDN? '),
targetcomment: enforceNonEmptyString('What are the target comments? '),
role: enforceEnum('What is the computing role?', Object.values(Role)),
assettype: enforceEnum('What is the asset type?', Object.values(Assettype)),
// Resulting techarea options have typos. Yes, these typos really are how the enumerations are defined in STIG viewer's source code
techarea: enforceEnum('What is the tech area? ', Object.values(Techarea)),
stigguid: enforceNonEmptyString('What is the STIG ID? '),
targetkey: enforceNonEmptyString('What is the target key? '),
webordatabase: String(enforceEnum('Is the target a web or database?', ['y', 'n']) === 'y'),
webdbsite: enforceNonEmptyString('What is the Web or DB site? '),
webdbinstance: enforceNonEmptyString('What is the Web or DB instance? '),
vulidmapping: enforceEnum('Use gid or id for vuln number?', ['gid', 'id']),
}
const validationResults = validateChecklistMetadata(cklMetadata as ChecklistMetadata)
if (validationResults.ok) {
fs.writeFileSync(flags.output, JSON.stringify(cklMetadata))
console.log(`Checklist metadata file written at: ${path.resolve(flags.output)}`)
} else {
console.error(`Unable to generate checklist metadata:\n${validationResults.error.message}`)
}
fs.writeFileSync(flags.output, JSON.stringify(cklMetadata))
}
}
Loading
Loading