diff --git a/src/commands/supplement/tags/read-obj.ts b/src/commands/supplement/tags/read-obj.ts new file mode 100644 index 000000000..89734a7d0 --- /dev/null +++ b/src/commands/supplement/tags/read-obj.ts @@ -0,0 +1,38 @@ +import {Command, Flags} from '@oclif/core' +import {processInSpecProfile, processExecJSON} from '@mitre/inspec-objects' +import fs from 'fs' +import {ExecJSON, ProfileJSON} from 'inspecjs' +import Profile from '@mitre/inspec-objects/lib/objects/profile' +import Control from '@mitre/inspec-objects/lib/objects/control' + +export default class ReadTags extends Command { + static usage = 'supplement tags read -i [-o ] [-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 updatedInput = Object.hasOwn((input), 'profiles') ? processExecJSON(input as ExecJSON.Execution) : processInSpecProfile(fs.readFileSync(flags.input, 'utf8')) + + const extractTags = (profile: Profile) => (profile.controls as Control[]).filter(control => flags.controls ? flags.controls.includes(control.id) : true).map(control => control.tags) + + const tags = extractTags(updatedInput) + + if (flags.output) { + fs.writeFileSync(flags.output, JSON.stringify(tags, null, 2)) + } else { + process.stdout.write(JSON.stringify(tags, null, 2)) + } + } +} diff --git a/src/commands/supplement/tags/read.ts b/src/commands/supplement/tags/read.ts new file mode 100644 index 000000000..eeab16fb1 --- /dev/null +++ b/src/commands/supplement/tags/read.ts @@ -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 [-o ] [-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).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)) + } + } +} diff --git a/src/commands/supplement/tags/write.ts b/src/commands/supplement/tags/write.ts new file mode 100644 index 000000000..5776a7e72 --- /dev/null +++ b/src/commands/supplement/tags/write.ts @@ -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 (-f | -d ) [-o ]' + + 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)?.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') + } +} +