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

Supplement tags #1381

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
97 changes: 97 additions & 0 deletions src/commands/supplement/tags/extend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {Command, Flags} from '@oclif/core'
import {ExecJSON, ProfileJSON} from 'inspecjs'
import fs from 'fs'
import csvParse from 'csv-parse'

export default class WriteTags extends Command {
static usage = 'supplement tags extend -i <input-hdf-or-profile-json> (-f <input-tags-json> | -d <tags-json>) [-o <output-hdf-json>]'

static description = 'Extends the `tags` attribute in a given Heimdall Data Format or InSpec Profile JSON file and overwrite original file or optionally output it to a new file'

static summary = 'Tags data can be either be a CSV file or JSON data. See sample ideas at https://github.com/mitre/saf/wiki/Supplement-HDF-files-with-additional-information-(ex.-%60tags%60,-%60target%60)'

static examples = [
'saf supplement tags extend -i hdf.json -d \'[[{"a": 5}]]\'',
'saf supplement tags extend -i hdf.json -f tags.csv -o new-hdf.json',
'saf supplement tags extend -i hdf.json -f tags.csv -o new-hdf.json -c "V-000001',
]

static flags = {
help: Flags.help({char: 'h'}),
input: Flags.string({char: 'i', required: true, description: 'An input HDF or profile file'}),
tagsFile: Flags.string({char: 'f', exclusive: ['tagsData'], description: 'An input tags-data file (can contain CSV file)); this flag or `tagsData` must be provided'}),
tagsData: Flags.string({char: 'd', exclusive: ['tagsFile'], description: 'Input tags-data (can contain JSON that matches structure of tags in input file(HDF or profile)); this flag or `tagsFile` must be provided'}),
output: Flags.string({char: 'o', description: 'An output file that matches structure of input file (otherwise the input file is overwritten)'}),
controls: Flags.string({char: 'c', description: 'The id of the control whose tags will be extracted', multiple: true}),
}

async run() {
const {flags} = await this.parse(WriteTags)

const input: ExecJSON.Execution | ProfileJSON.Profile = JSON.parse(fs.readFileSync(flags.input, 'utf8'))

const output: string = flags.output || flags.input

let CCItags: object | string
// TODO: Make more generic
if (flags.tagsFile) {
try {
const fileContent = fs.readFileSync(flags.tagsFile, 'utf8')
csvParse(fileContent, {columns: true, delimiter: ','}, (err, output) => {
if (err) {
throw new Error(`CSV parse error ${err}`)
}

CCItags = JSON.parse(JSON.stringify(output))
// TODO: Right now passing into function. When parsing csv add proper await function
processParsedData(CCItags)
})
} catch (error: unknown) {
throw new Error(`Couldn't parse tags data: ${error}`)
}
} else if (flags.tagsData) {
try {
CCItags = JSON.parse(flags.tagsData)
} catch {
CCItags = flags.tagsData
}

processParsedData(CCItags)
} else {
throw new Error('One out of tagsFile or tagsData must be passed')
}

const extendTags = (profile: ExecJSON.Profile | ProfileJSON.Profile, CCItags: []) => {
// Filter our controls
const filteredControls = (profile.controls as Array<ExecJSON.Control | ProfileJSON.Control>)?.filter(control => flags.controls ? flags.controls.includes(control.id) : true)
for (const tag of filteredControls.map(control => control.tags)) {
if (tag.cci) {
const cms_ars5_ce: string[] = []
for (const cci of tag.cci) {
// TODO: Currently striping whitespace might not need this
const matchingTag = CCItags.find((currTag: { cci: any }) => currTag.cci.replace(/\s/g, '').includes(cci))
if (matchingTag && matchingTag['cms-ars5-ce'] !== '') {
cms_ars5_ce.push(matchingTag['cms-ars5-ce'])
}
}

if (cms_ars5_ce.length !== 0)
tag.cms_ars5_ce = cms_ars5_ce
}
}
}

function processParsedData(CCItags: any) {
if (Object.hasOwn(input, 'profiles')) {
for (const [i, profile] of (input as ExecJSON.Execution).profiles.entries()) {
extendTags(profile, CCItags)
}
} else {
extendTags((input as ProfileJSON.Profile), CCItags)
}

fs.writeFileSync(output, JSON.stringify(input, null, 2))
console.log('Tags successfully extended')
}
}
}
34 changes: 34 additions & 0 deletions src/commands/supplement/tags/read.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Command, Flags} from '@oclif/core'
import {ExecJSON, ProfileJSON} from 'inspecjs'
import fs from 'fs'

export default class ReadTags extends Command {
static usage = 'supplement tags read -i <hdf-or-profile-json> [-o <tag-json>] [-c control-id ...]'

static description = 'Read the `tags` attribute in a given Heimdall Data Format or InSpec Profile JSON file and send it to stdout or write it to a file'

static examples = ['saf supplement tags read -i hdf.json -o tag.json', 'saf supplement tags read -i hdf.json -o tag.json -c V-00001 V-00002']

static flags = {
help: Flags.help({char: 'h'}),
input: Flags.string({char: 'i', required: true, description: 'An input HDF or profile file'}),
output: Flags.string({char: 'o', description: 'An output `tags` JSON file (otherwise the data is sent to stdout)'}),
controls: Flags.string({char: 'c', description: 'The id of the control whose tags will be extracted', multiple: true}),
}

async run() {
const {flags} = await this.parse(ReadTags)

const input: ExecJSON.Execution | ProfileJSON.Profile = JSON.parse(fs.readFileSync(flags.input, 'utf8'))

const extractTags = (profile: ExecJSON.Profile | ProfileJSON.Profile) => (profile.controls as Array<ExecJSON.Control | ProfileJSON.Control>).filter(control => flags.controls ? flags.controls.includes(control.id) : true).map(control => control.tags)

const tags = Object.hasOwn(input, 'profiles') ? (input as ExecJSON.Execution).profiles.map(profile => extractTags(profile)) : extractTags(input as ProfileJSON.Profile)

if (flags.output) {
fs.writeFileSync(flags.output, JSON.stringify(tags, null, 2))
} else {
process.stdout.write(JSON.stringify(tags, null, 2))
}
}
}
77 changes: 77 additions & 0 deletions src/commands/supplement/tags/write.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {Command, Flags} from '@oclif/core'
import {ExecJSON, ProfileJSON} from 'inspecjs'
import fs from 'fs'

export default class WriteTags extends Command {
static usage = 'supplement tags write -i <input-hdf-or-profile-json> (-f <input-tags-json> | -d <tags-json>) [-o <output-hdf-json>]'

static description = 'Overwrite the `tags` attribute in a given Heimdall Data Format or InSpec Profile JSON file and overwrite original file or optionally write it to a new file'

static summary = 'Tags data can be either a Heimdall Data Format or InSpec Profile JSON file. See sample ideas at https://github.com/mitre/saf/wiki/Supplement-HDF-files-with-additional-information-(ex.-%60tags%60,-%60target%60)'

static examples = [
'saf supplement tags write -i hdf.json -d \'[[{"a": 5}]]\'',
'saf supplement tags write -i hdf.json -f tags.json -o new-hdf.json',
'saf supplement tags write -i hdf.json -f tags.json -o new-hdf.json -c "V-000001',
]

static flags = {
help: Flags.help({char: 'h'}),
input: Flags.string({char: 'i', required: true, description: 'An input HDF or profile file'}),
tagsFile: Flags.string({char: 'f', exclusive: ['tagsData'], description: 'An input tags-data file (can contain JSON that matches structure of tags in input file(HDF or profile)); this flag or `tagsData` must be provided'}),
tagsData: Flags.string({char: 'd', exclusive: ['tagsFile'], description: 'Input tags-data (can contain JSON that matches structure of tags in input file(HDF or profile)); this flag or `tagsFile` must be provided'}),
output: Flags.string({char: 'o', description: 'An output file that matches structure of input file (otherwise the input file is overwritten)'}),
controls: Flags.string({char: 'c', description: 'The id of the control whose tags will be extracted', multiple: true}),
}

async run() {
const {flags} = await this.parse(WriteTags)

const input: ExecJSON.Execution | ProfileJSON.Profile = JSON.parse(fs.readFileSync(flags.input, 'utf8'))

const output: string = flags.output || flags.input

let tags: ExecJSON.Control[][] | ProfileJSON.Control[] | string
if (flags.tagsFile) {
try {
tags = JSON.parse(fs.readFileSync(flags.tagsFile, 'utf8'))
} catch (error: unknown) {
throw new Error(`Couldn't parse tags data: ${error}`)
}
} else if (flags.tagsData) {
try {
tags = JSON.parse(flags.tagsData)
} catch {
tags = flags.tagsData
}
} else {
throw new Error('One out of tagsFile or tagsData must be passed')
}

const overwriteTags = (profile: ExecJSON.Profile | ProfileJSON.Profile, tags: ExecJSON.Control[] | ProfileJSON.Control[]) => {
// Filter our controls
const filteredControls = (profile.controls as Array<ExecJSON.Control | ProfileJSON.Control>)?.filter(control => flags.controls ? flags.controls.includes(control.id) : true)
// Check shape
if (filteredControls.length !== tags.length) {
throw new TypeError('Structure of tags data is invalid')
}

// Overwrite tags
for (const [index, control] of filteredControls.entries()) {
control.tags = tags[index]
}
}

if (Object.hasOwn(input, 'profiles')) {
for (const [i, profile] of (input as ExecJSON.Execution).profiles.entries()) {
overwriteTags(profile, tags[i] as ExecJSON.Control[])
}
} else {
overwriteTags((input as ProfileJSON.Profile), (tags as ProfileJSON.Control[]))
}

fs.writeFileSync(output, JSON.stringify(input, null, 2))
console.log('Tags successfully overwritten')
}
}