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 with ts-inspec-objects #1391

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
38 changes: 38 additions & 0 deletions src/commands/supplement/tags/read-obj.ts
Original file line number Diff line number Diff line change
@@ -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 <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 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))
}
}
}
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')
}
}