Skip to content

Commit 750be3f

Browse files
committed
feat: add finding details actions
1 parent 3a7e6f2 commit 750be3f

File tree

10 files changed

+132
-24
lines changed

10 files changed

+132
-24
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ You can quickly navigate through all partially audited regions in your workspace
8989

9090
You can fill detailed information about a finding by clicking on it in the _List of Findings_ view in the sidebar. The respective _Finding Details_ panel will open, where you can fill the information.
9191
The panel also shows a read-only provenance field (defaulting to "human").
92+
The action buttons at the top let you triage findings (True/False Positive), resolve notes, or open a GitHub issue.
9293

9394
![Finding Details](media/readme/finding_details.png)
9495

@@ -106,7 +107,7 @@ You can add multiple regions to a single finding or note. Once you select the co
106107

107108
### Resolve and Restore
108109

109-
Notes can be resolved from the _List of Findings_ panel. Findings are triaged instead: mark them as `True Positive` or `False Negative` from the same panel. Resolved notes and triaged findings are no longer highlighted in the editor but remain visible in the _Resolved Findings_ panel with a status badge. You can restore any resolved entry by clicking the corresponding `Restore` button in the _Resolved Findings_ panel.
110+
Notes can be resolved from the _List of Findings_ panel. Findings are triaged instead: mark them as `True Positive` or `False Positive` from the same panel. Resolved notes and triaged findings are no longer highlighted in the editor but remain visible in the _Resolved Findings_ panel with a status badge. You can restore any resolved entry by clicking the corresponding `Restore` button in the _Resolved Findings_ panel.
110111

111112
![Resolve and Restore](media/readme/gifs/resolve_finding.gif)
112113

media/style.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
flex: 1;
1313
}
1414

15+
.detailsActions {
16+
display: flex;
17+
flex-wrap: wrap;
18+
gap: 0.5em;
19+
padding-bottom: 1em;
20+
}
21+
1522
vscode-dropdown,
1623
vscode-text-area,
1724
vscode-text-field {

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@
150150
"icon": "$(check)"
151151
},
152152
{
153-
"command": "weAudit.markFalseNegative",
154-
"title": "Mark False Negative",
153+
"command": "weAudit.markFalsePositive",
154+
"title": "Mark False Positive",
155155
"icon": "$(close)"
156156
},
157157
{
@@ -329,7 +329,7 @@
329329
"when": "false"
330330
},
331331
{
332-
"command": "weAudit.markFalseNegative",
332+
"command": "weAudit.markFalsePositive",
333333
"when": "false"
334334
},
335335
{
@@ -459,7 +459,7 @@
459459
"group": "inline@5"
460460
},
461461
{
462-
"command": "weAudit.markFalseNegative",
462+
"command": "weAudit.markFalsePositive",
463463
"when": "view == codeMarker && viewItem == finding",
464464
"group": "inline@6"
465465
},

src/codeMarker.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,8 +1759,13 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
17591759
this.markTruePositive(node);
17601760
});
17611761

1762+
vscode.commands.registerCommand("weAudit.markFalsePositive", (node: FullEntry) => {
1763+
this.markFalsePositive(node);
1764+
});
1765+
1766+
// Legacy alias for backward compatibility.
17621767
vscode.commands.registerCommand("weAudit.markFalseNegative", (node: FullEntry) => {
1763-
this.markFalseNegative(node);
1768+
this.markFalsePositive(node);
17641769
});
17651770

17661771
vscode.commands.registerCommand("weAudit.deleteFinding", (node: FullEntry) => {
@@ -2953,7 +2958,7 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
29532958
resolveFinding(entry: FullEntry): void {
29542959
// Notes can be resolved; findings must be triaged as TP/FN.
29552960
if (entry.entryType !== EntryType.Note) {
2956-
vscode.window.showInformationMessage("Findings cannot be resolved. Mark them as True Positive or False Negative instead.");
2961+
vscode.window.showInformationMessage("Findings cannot be resolved. Mark them as True Positive or False Positive instead.");
29572962
return;
29582963
}
29592964
this.applyEntryResolution(entry, EntryResolution.Resolved, true);
@@ -2976,13 +2981,13 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
29762981
* Marks a finding as a false negative and moves it to the resolved entries list.
29772982
* @param entry the finding to mark.
29782983
*/
2979-
private markFalseNegative(entry: FullEntry): void {
2980-
// Findings only: use FN to close the entry.
2984+
private markFalsePositive(entry: FullEntry): void {
2985+
// Findings only: use FP to close the entry.
29812986
if (entry.entryType !== EntryType.Finding) {
2982-
vscode.window.showInformationMessage("Only findings can be marked as False Negative.");
2987+
vscode.window.showInformationMessage("Only findings can be marked as False Positive.");
29832988
return;
29842989
}
2985-
this.applyEntryResolution(entry, EntryResolution.FalseNegative, true);
2990+
this.applyEntryResolution(entry, EntryResolution.FalsePositive, true);
29862991
}
29872992

29882993
/**
@@ -2999,7 +3004,7 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
29993004
return;
30003005
}
30013006

3002-
if (resolution === EntryResolution.Open || resolution === EntryResolution.TruePositive || resolution === EntryResolution.FalseNegative) {
3007+
if (resolution === EntryResolution.Open || resolution === EntryResolution.TruePositive || resolution === EntryResolution.FalsePositive) {
30033008
return resolution;
30043009
}
30053010
return;
@@ -3680,9 +3685,14 @@ export class CodeMarker implements vscode.TreeDataProvider<TreeEntry> {
36803685
}
36813686

36823687
// Legacy resolved findings default to Unclassified unless already triaged.
3688+
const resolutionValue = String(entry.details.resolution);
3689+
if (resolutionValue === "False Negative") {
3690+
entry.details.resolution = EntryResolution.FalsePositive;
3691+
}
3692+
36833693
if (
36843694
entry.details.resolution !== EntryResolution.TruePositive &&
3685-
entry.details.resolution !== EntryResolution.FalseNegative &&
3695+
entry.details.resolution !== EntryResolution.FalsePositive &&
36863696
entry.details.resolution !== EntryResolution.Unclassified
36873697
) {
36883698
entry.details.resolution = EntryResolution.Unclassified;

src/panels/findingDetails.html

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
<div id="container-div">
2+
<div class="detailsActions" id="finding-actions">
3+
<vscode-button id="action-mark-true-positive">Mark True Positive</vscode-button>
4+
<vscode-button id="action-mark-false-positive">Mark False Positive</vscode-button>
5+
<vscode-button id="action-open-github-issue">Open Github Issue</vscode-button>
6+
</div>
7+
<div class="detailsActions" id="note-actions">
8+
<vscode-button id="action-resolve-note">Resolve</vscode-button>
9+
<vscode-button id="action-open-github-issue-note">Open Github Issue</vscode-button>
10+
</div>
211
<div class="detailsDiv">
312
<span class="detailSpan">Title:</span>
413
<vscode-text-field id="label-area"></vscode-text-field>
@@ -13,7 +22,7 @@
1322
<vscode-dropdown position="below" id="resolution-finding-dropdown">
1423
<vscode-option>Open</vscode-option>
1524
<vscode-option>True Positive</vscode-option>
16-
<vscode-option>False Negative</vscode-option>
25+
<vscode-option>False Positive</vscode-option>
1726
</vscode-dropdown>
1827
</div>
1928

src/panels/findingDetailsPanel.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,23 @@ class FindingDetailsProvider implements vscode.WebviewViewProvider {
133133
case "update-entry":
134134
vscode.commands.executeCommand("weAudit.updateCurrentSelectedEntry", message.field, message.value, message.isPersistent);
135135
return;
136+
case "details-action": {
137+
switch (message.action) {
138+
case "mark-true-positive":
139+
vscode.commands.executeCommand("weAudit.updateCurrentSelectedEntry", "resolution", "True Positive", true);
140+
return;
141+
case "mark-false-positive":
142+
vscode.commands.executeCommand("weAudit.updateCurrentSelectedEntry", "resolution", "False Positive", true);
143+
return;
144+
case "resolve-note":
145+
vscode.commands.executeCommand("weAudit.updateCurrentSelectedEntry", "resolution", "Resolved", true);
146+
return;
147+
case "open-github-issue":
148+
vscode.commands.executeCommand("weAudit.openGithubIssueFromDetails");
149+
return;
150+
}
151+
return;
152+
}
136153
case "webview-ready":
137154
// When the webview reports it's ready, update with current data
138155
vscode.commands.executeCommand("weAudit.showSelectedEntryInFindingDetails");

src/resolvedFindings.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ function getResolutionBadge(entry: FullEntry): string {
1616
if (entry.details?.resolution === EntryResolution.TruePositive) {
1717
return "TP";
1818
}
19-
if (entry.details?.resolution === EntryResolution.FalseNegative) {
20-
return "FN";
19+
const resolutionValue = String(entry.details?.resolution);
20+
if (resolutionValue === "False Positive" || resolutionValue === "False Negative") {
21+
return "FP";
2122
}
2223
if (entry.details?.resolution === EntryResolution.Unclassified) {
2324
return "UNCLASSIFIED";

src/types.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export enum EntryResolution {
2121
Open = "Open",
2222
Resolved = "Resolved",
2323
TruePositive = "True Positive",
24-
FalseNegative = "False Negative",
24+
FalsePositive = "False Positive",
2525
Unclassified = "Unclassified",
2626
}
2727

@@ -197,7 +197,10 @@ function validateEntryDetails(entryDetails: EntryDetails): boolean {
197197
typeof entryDetails.provenance.created === "string" &&
198198
(typeof entryDetails.provenance.campaign === "string" || entryDetails.provenance.campaign === null) &&
199199
typeof entryDetails.provenance.commitHash === "string");
200-
const resolutionValid = entryDetails.resolution === undefined || isEntryResolution(entryDetails.resolution);
200+
const resolutionValid =
201+
entryDetails.resolution === undefined ||
202+
isEntryResolution(entryDetails.resolution) ||
203+
entryDetails.resolution === "False Negative";
201204
return (
202205
entryDetails.severity !== undefined &&
203206
entryDetails.difficulty !== undefined &&
@@ -264,7 +267,7 @@ export function isEntryResolution(value: string | undefined): value is EntryReso
264267
value === EntryResolution.Open ||
265268
value === EntryResolution.Resolved ||
266269
value === EntryResolution.TruePositive ||
267-
value === EntryResolution.FalseNegative ||
270+
value === EntryResolution.FalsePositive ||
268271
value === EntryResolution.Unclassified
269272
);
270273
}

src/webview/findingDetailsMain.ts

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
2-
import { provideVSCodeDesignSystem, vsCodeDropdown, vsCodeTextArea, vsCodeOption, vsCodeTextField } from "@vscode/webview-ui-toolkit";
3-
import { TextArea, Dropdown, TextField } from "@vscode/webview-ui-toolkit";
4-
import { UpdateEntryMessage } from "./webviewMessageTypes";
2+
import {
3+
provideVSCodeDesignSystem,
4+
vsCodeDropdown,
5+
vsCodeTextArea,
6+
vsCodeOption,
7+
vsCodeTextField,
8+
vsCodeButton,
9+
} from "@vscode/webview-ui-toolkit";
10+
import { TextArea, Dropdown, TextField, Button } from "@vscode/webview-ui-toolkit";
11+
import { DetailsActionMessage, UpdateEntryMessage } from "./webviewMessageTypes";
512

613
// In order to use all the Webview UI Toolkit web components they
714
// must be registered with the browser (i.e. webview) using the
815
// syntax below.
916
// provideVSCodeDesignSystem().register(allComponents);
10-
provideVSCodeDesignSystem().register(vsCodeDropdown(), vsCodeTextArea(), vsCodeOption(), vsCodeTextField());
17+
provideVSCodeDesignSystem().register(vsCodeDropdown(), vsCodeTextArea(), vsCodeOption(), vsCodeTextField(), vsCodeButton());
1118

1219
const vscode = acquireVsCodeApi();
1320

@@ -26,6 +33,20 @@ function main(): void {
2633

2734
const provenanceValue = document.getElementById("provenance-value") as HTMLSpanElement;
2835

36+
const findingActionsRow = document.getElementById("finding-actions") as HTMLDivElement;
37+
const noteActionsRow = document.getElementById("note-actions") as HTMLDivElement;
38+
const markTruePositiveButton = document.getElementById("action-mark-true-positive") as Button | null;
39+
const markFalsePositiveButton = document.getElementById("action-mark-false-positive") as Button | null;
40+
const openGithubIssueButton = document.getElementById("action-open-github-issue") as Button | null;
41+
const resolveNoteButton = document.getElementById("action-resolve-note") as Button | null;
42+
const openGithubIssueNoteButton = document.getElementById("action-open-github-issue-note") as Button | null;
43+
44+
registerActionButton(markTruePositiveButton, "mark-true-positive");
45+
registerActionButton(markFalsePositiveButton, "mark-false-positive");
46+
registerActionButton(openGithubIssueButton, "open-github-issue");
47+
registerActionButton(resolveNoteButton, "resolve-note");
48+
registerActionButton(openGithubIssueNoteButton, "open-github-issue");
49+
2950
const resolutionFindingRow = document.getElementById("resolution-row-finding") as HTMLDivElement;
3051
const resolutionNoteRow = document.getElementById("resolution-row-note") as HTMLDivElement;
3152
const resolutionFindingDropdown = document.getElementById("resolution-finding-dropdown") as Dropdown;
@@ -66,6 +87,8 @@ function main(): void {
6687
containerDiv.style.display = "none";
6788
resolutionFindingRow.style.display = "none";
6889
resolutionNoteRow.style.display = "none";
90+
findingActionsRow.style.display = "none";
91+
noteActionsRow.style.display = "none";
6992

7093
// handle the message inside the webview
7194
window.addEventListener("message", (event) => {
@@ -79,6 +102,8 @@ function main(): void {
79102
setResolutionControls(
80103
message.entryType as string | undefined,
81104
message.resolution as string | undefined,
105+
findingActionsRow,
106+
noteActionsRow,
82107
resolutionFindingRow,
83108
resolutionNoteRow,
84109
resolutionFindingDropdown,
@@ -98,6 +123,8 @@ function main(): void {
98123
provenanceValue.textContent = "";
99124
resolutionFindingRow.style.display = "none";
100125
resolutionNoteRow.style.display = "none";
126+
findingActionsRow.style.display = "none";
127+
noteActionsRow.style.display = "none";
101128
break;
102129
}
103130
});
@@ -109,15 +136,19 @@ function main(): void {
109136
function setResolutionControls(
110137
entryType: string | undefined,
111138
resolution: string | undefined,
139+
findingActionsRow: HTMLDivElement,
140+
noteActionsRow: HTMLDivElement,
112141
resolutionFindingRow: HTMLDivElement,
113142
resolutionNoteRow: HTMLDivElement,
114143
resolutionFindingDropdown: Dropdown,
115144
resolutionNoteDropdown: Dropdown,
116145
): void {
117146
const isFinding = entryType === "finding";
118-
const findingResolution = coerceResolutionValue(resolution, ["Open", "True Positive", "False Negative"], "Open");
147+
const findingResolution = coerceResolutionValue(resolution, ["Open", "True Positive", "False Positive"], "Open");
119148
const noteResolution = coerceResolutionValue(resolution, ["Open", "Resolved"], "Open");
120149

150+
findingActionsRow.style.display = isFinding ? "flex" : "none";
151+
noteActionsRow.style.display = isFinding ? "none" : "flex";
121152
resolutionFindingRow.style.display = isFinding ? "flex" : "none";
122153
resolutionNoteRow.style.display = isFinding ? "none" : "flex";
123154
resolutionFindingDropdown.value = findingResolution;
@@ -155,3 +186,26 @@ function handleFieldChange(e: Event, isPersistent: boolean): void {
155186
};
156187
vscode.postMessage(message);
157188
}
189+
190+
/**
191+
* Registers a button click listener that posts a details action message.
192+
*/
193+
function registerActionButton(button: Button | null, action: DetailsActionMessage["action"]): void {
194+
if (!button) {
195+
return;
196+
}
197+
button.addEventListener("click", () => {
198+
postDetailsAction(action);
199+
});
200+
}
201+
202+
/**
203+
* Posts a details action message to the extension host.
204+
*/
205+
function postDetailsAction(action: DetailsActionMessage["action"]): void {
206+
const message: DetailsActionMessage = {
207+
command: "details-action",
208+
action: action,
209+
};
210+
vscode.postMessage(message);
211+
}

src/webview/webviewMessageTypes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export type WebviewMessage =
22
| UpdateEntryMessage
3+
| DetailsActionMessage
34
| UpdateRepositoryMessage
45
| UpdateSyncConfigMessage
56
| SyncNowMessage
@@ -15,6 +16,11 @@ export interface UpdateEntryMessage {
1516
isPersistent: boolean;
1617
}
1718

19+
export interface DetailsActionMessage {
20+
command: "details-action";
21+
action: "mark-true-positive" | "mark-false-positive" | "resolve-note" | "open-github-issue";
22+
}
23+
1824
export interface UpdateRepositoryMessage {
1925
command: "update-repository-config";
2026
rootLabel: string;

0 commit comments

Comments
 (0)