-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Assign triage status to findings. Each finding is identified by hash_v2. Finding status can be: * deferred * as-designed * active Findings status is saved to file appmap-scanner-state.yml.
- Loading branch information
Showing
5 changed files
with
170 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { Argv } from 'yargs'; | ||
|
||
export function directoryArg(args: Argv<{}>): void { | ||
args.option('directory', { | ||
describe: 'program working directory', | ||
type: 'string', | ||
alias: 'd', | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import { Arguments, Argv } from 'yargs'; | ||
import readline from 'readline'; | ||
|
||
import { verbose } from '../../rules/lib/util'; | ||
|
||
import CommandOptions from './options'; | ||
import { handleWorkingDirectory } from '../handleWorkingDirectory'; | ||
import { readFile, writeFile } from 'fs/promises'; | ||
import { dump, load } from 'js-yaml'; | ||
import { directoryArg } from '../directoryArg'; | ||
import { exists } from '../../../../cli/src/utils'; | ||
|
||
enum FindingState { | ||
// Finding is valid, accepted and should be fixed. | ||
Active = 'active', | ||
// Finding will not be acted upon at this time. The comment should clarify why this is. | ||
Deferred = 'deferred', | ||
// Finding is a false positive. The code is working as designed. | ||
AsDesigned = 'as-designed', | ||
} | ||
|
||
type TriagedFinding = Record<FindingState, TriageItem[]>; | ||
|
||
interface TriageItem { | ||
hash_v2: string; | ||
comment?: string; | ||
updated_at: Date; | ||
} | ||
|
||
export default { | ||
command: 'triage <finding...>', | ||
describe: 'Triage findings by assigning them to a workflow state', | ||
builder(args: Argv): Argv { | ||
directoryArg(args); | ||
|
||
args.option('state-file', { | ||
describe: 'Name of the file containing the findings state', | ||
type: 'string', | ||
default: 'appmap-findings-state.yml', | ||
}); | ||
|
||
args.option('comment', { | ||
describe: 'Comment to associate with the triage action', | ||
alias: ['c'], | ||
}); | ||
|
||
args.option('state', { | ||
describe: 'Workflow state to assign to the finding', | ||
type: 'string', | ||
demandOption: true, | ||
options: ['active', 'deferred', 'as-designed'], | ||
alias: ['s'], | ||
}); | ||
|
||
args.positional('finding', { | ||
describe: 'Identifying hash (hash_v2 digest) of the finding', | ||
type: 'string', | ||
array: true, | ||
}); | ||
|
||
return args.strict(); | ||
}, | ||
async handler(options: Arguments): Promise<void> { | ||
let { comment } = options as unknown as CommandOptions; | ||
const { | ||
stateFile: stateFileName, | ||
directory, | ||
verbose: isVerbose, | ||
state: stateStr, | ||
finding: findingHashArray, | ||
} = options as unknown as CommandOptions; | ||
|
||
if (isVerbose) { | ||
verbose(true); | ||
} | ||
|
||
handleWorkingDirectory(directory); | ||
const assignedState = stateStr as FindingState; | ||
|
||
const findingHashes = new Set<string>(findingHashArray); | ||
|
||
let triagedFindings: TriagedFinding; | ||
if (await exists(stateFileName)) { | ||
triagedFindings = load(await readFile(stateFileName, 'utf8')) as TriagedFinding; | ||
} else { | ||
triagedFindings = { | ||
[FindingState.Active]: [], | ||
[FindingState.Deferred]: [], | ||
[FindingState.AsDesigned]: [], | ||
} as TriagedFinding; | ||
} | ||
|
||
if (!comment) { | ||
comment = await promptForComment(); | ||
} | ||
|
||
const existingTriageItems = new Map<string, TriageItem>(); | ||
// Remove any findings that are previously triaged and will now be recategorized. | ||
[FindingState.Active, FindingState.Deferred, FindingState.AsDesigned].forEach((state) => { | ||
triagedFindings[state] = triagedFindings[state].filter((triagedFinding) => { | ||
if (findingHashes.has(triagedFinding.hash_v2)) { | ||
triagedFinding.comment = comment; | ||
triagedFinding.updated_at = new Date(Date.now()); | ||
existingTriageItems.set(triagedFinding.hash_v2, triagedFinding); | ||
return false; | ||
} | ||
return true; | ||
}); | ||
}); | ||
|
||
findingHashes.forEach((hash) => { | ||
let triagedFinding = existingTriageItems.get(hash); | ||
if (!triagedFinding) { | ||
triagedFinding = { | ||
hash_v2: hash, | ||
comment, | ||
updated_at: new Date(Date.now()), | ||
}; | ||
} | ||
|
||
triagedFindings[assignedState].push(triagedFinding); | ||
}); | ||
|
||
triagedFindings[assignedState] = triagedFindings[assignedState].sort((a, b) => { | ||
let diff = b.updated_at.getTime() - a.updated_at.getTime(); | ||
if (diff === 0) diff = a.hash_v2.localeCompare(b.hash_v2); | ||
return diff; | ||
}); | ||
|
||
await writeFile(stateFileName, dump(triagedFindings)); | ||
}, | ||
}; | ||
|
||
async function promptForComment(): Promise<string | undefined> { | ||
if (process.stdout.isTTY) { | ||
const ui = readline.createInterface({ | ||
input: process.stdin, | ||
output: process.stdout, | ||
}); | ||
|
||
return new Promise<string | undefined>((resolve) => | ||
ui.question('Comment (optional): ', resolve) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export default interface CommandOptions { | ||
directory?: string; | ||
stateFile: string; | ||
verbose?: boolean; | ||
finding: string[]; | ||
state: string; | ||
comment?: string; | ||
} |