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
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))
}
}
}
92 changes: 92 additions & 0 deletions src/commands/supplement/tags/write.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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: any
GavMason marked this conversation as resolved.
Show resolved Hide resolved
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: any) => {
// 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
console.log(profile.controls.length)
GavMason marked this conversation as resolved.
Show resolved Hide resolved
console.log(tags.length)
if (!flags.controls && profile.controls.length !== tags.length) {
GavMason marked this conversation as resolved.
Show resolved Hide resolved
throw new TypeError('Structure of tags data is invalid')
}

// Overwrite tags
const updatedControls = profile.controls.map((control: any, index: number) => {
GavMason marked this conversation as resolved.
Show resolved Hide resolved
if (filteredControls.includes(control)) {
return {
...control,
tags: tags[index],
GavMason marked this conversation as resolved.
Show resolved Hide resolved
}
}

return control
})
return updatedControls
}

if (Object.hasOwn(input, 'profiles')) {
for (const [i, profile] of (input as ExecJSON.Execution).profiles.entries()) {
const updatedControls = overwriteTags(profile, tags[i])

profile.controls = updatedControls
}
} else {
const updatedControls = overwriteTags((input as ProfileJSON.Profile), tags);

(input as ProfileJSON.Profile).controls = updatedControls
}

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