Skip to content

Commit a3b558f

Browse files
jess-lowegoogle-labs-jules[bot]gemini-code-assist[bot]
authored andcommitted
feat: CVE conversion triager tool (google#4916)
Quite often do we need to compare multiple records of one CVE record to triage conversion issues, so this tool will allow us to compare three versions at a time. <img width="2557" height="1185" alt="image" src="https://github.com/user-attachments/assets/beb213fd-a2a0-4fe0-895d-1454bccc722b" /> You can select from the following record locations: - the og cve5 record - the og NVD record - the nvd-converted osv record in prod - the nvd-conversion metrics notes in prod - the nvd-converted osv record in test - the nvd-conversion metrics notes in test - the cve5-converted osv record in prod - the cve5-conversion metrics notes in prod - the cve5-converted osv record in test - the cve5-conversion metrics notes in test - the combined output in prod - the combined output in test - prod api - test api It currently exists at /triage, and utilises a proxy mechanism to load the records, but not as a path (as to prevent accessing unrelated parts of the bucket) In the future could add a tool for an LLM to help triage what the actual problem is, but that's for another day --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 331643f commit a3b558f

7 files changed

Lines changed: 509 additions & 2 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{% extends 'base.html' %}
2+
{% set active_section = 'triage' %}
3+
4+
{% block content %}
5+
<div class="triage-container">
6+
<div class="header-container">
7+
<h1>CVE Conversion Triager</h1>
8+
9+
<div class="input-section">
10+
<md-filled-text-field id="vuln-id-input" label="Vulnerability ID" placeholder="e.g. CVE-2023-1234" class="standard-input"></md-filled-text-field>
11+
<md-filled-button id="load-btn">Load</md-filled-button>
12+
</div>
13+
</div>
14+
<div class="triage-grid">
15+
{% for i in range(1, 4) %}
16+
<div class="triage-column" id="column-{{ i }}">
17+
<div class="column-header">
18+
<label for="source-select-{{ i }}">Source:</label>
19+
<div class="select-wrapper">
20+
<select id="source-select-{{ i }}" class="source-select">
21+
<option value="">Select Source...</option>
22+
<optgroup label="External APIs">
23+
<option value="cve-org">CVE.org API</option>
24+
<option value="nvd-api">NVD API 2.0</option>
25+
</optgroup>
26+
<optgroup label="Test Instance (gs://osv-test-cve-osv-conversion)">
27+
<option value="test-nvd">NVD-OSV (JSON)</option>
28+
<option value="test-cve5">CVE5 (JSON)</option>
29+
<option value="test-osv">OSV Output (JSON)</option>
30+
<option value="test-nvd-metrics">NVD Metrics</option>
31+
<option value="test-cve5-metrics">CVE5 Metrics</option>
32+
</optgroup>
33+
<optgroup label="Prod Instance (gs://cve-osv-conversion)">
34+
<option value="prod-nvd">NVD-OSV (JSON)</option>
35+
<option value="prod-cve5">CVE5 (JSON)</option>
36+
<option value="prod-osv">OSV Output (JSON)</option>
37+
<option value="prod-nvd-metrics">NVD Metrics</option>
38+
<option value="prod-cve5-metrics">CVE5 Metrics</option>
39+
</optgroup>
40+
<optgroup label="API">
41+
<option value="api-test">api.test.osv.dev</option>
42+
<option value="api-prod">api.osv.dev</option>
43+
</optgroup>
44+
</select>
45+
</div>
46+
</div>
47+
<div class="content-display">
48+
<div class="loading-spinner hidden">
49+
<md-circular-progress indeterminate></md-circular-progress>
50+
</div>
51+
<pre class="json-content">Select a source to view content</pre>
52+
</div>
53+
</div>
54+
{% endfor %}
55+
</div>
56+
</div>
57+
{% endblock %}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import "./triage.scss";
2+
import "@material/web/textfield/filled-text-field.js";
3+
import "@material/web/button/filled-button.js";
4+
import "@material/web/progress/circular-progress.js";
5+
6+
document.addEventListener("DOMContentLoaded", () => {
7+
const vulnIdInput = document.getElementById("vuln-id-input");
8+
const loadBtn = document.getElementById("load-btn");
9+
const columns = document.querySelectorAll(".triage-column");
10+
11+
// Map selection values to their respective endpoints/paths
12+
const sourceConfigMap = {
13+
// External APIs
14+
"cve-org": {
15+
proxySource: "cve",
16+
},
17+
"nvd-api": {
18+
proxySource: "nvd",
19+
},
20+
// Test Instance
21+
"test-nvd": {
22+
proxySource: "test-nvd",
23+
},
24+
"test-cve5": {
25+
proxySource: "test-cve5",
26+
},
27+
"test-osv": {
28+
proxySource: "test-osv",
29+
},
30+
"test-nvd-metrics": {
31+
proxySource: "test-nvd-metrics",
32+
},
33+
"test-cve5-metrics": {
34+
proxySource: "test-cve5-metrics",
35+
},
36+
// Prod Instance
37+
"prod-nvd": {
38+
proxySource: "prod-nvd",
39+
},
40+
"prod-cve5": {
41+
proxySource: "prod-cve5",
42+
},
43+
"prod-osv": {
44+
proxySource: "prod-osv",
45+
},
46+
"prod-nvd-metrics": {
47+
proxySource: "prod-nvd-metrics",
48+
},
49+
"prod-cve5-metrics": {
50+
proxySource: "prod-cve5-metrics",
51+
},
52+
// API
53+
"api-test": {
54+
urlTemplate: "https://api.test.osv.dev/v1/vulns/{id}",
55+
},
56+
"api-prod": {
57+
urlTemplate: "https://api.osv.dev/v1/vulns/{id}",
58+
},
59+
};
60+
61+
function escapeHtml(text) {
62+
const div = document.createElement('div');
63+
div.textContent = text;
64+
return div.innerHTML;
65+
}
66+
67+
function syntaxHighlight(json) { // credit to https://codepen.io/absolutedevelopment/pen/EpwVzN
68+
if (typeof json !== 'string') {
69+
json = JSON.stringify(json, undefined, 2);
70+
}
71+
72+
const escapedJson = escapeHtml(json);
73+
74+
return escapedJson.replace(
75+
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
76+
function (match) {
77+
let cls = 'json-number';
78+
if (/^"/.test(match)) {
79+
if (/:$/.test(match)) {
80+
cls = 'json-key';
81+
} else {
82+
cls = 'json-string';
83+
}
84+
} else if (/true|false/.test(match)) {
85+
cls = 'json-boolean';
86+
} else if (/null/.test(match)) {
87+
cls = 'json-null';
88+
}
89+
return `<span class="${cls}">${match}</span>`;
90+
}
91+
);
92+
}
93+
94+
async function fetchData(sourceKey, vulnId) {
95+
const config = sourceConfigMap[sourceKey];
96+
let url;
97+
98+
const safeId = encodeURIComponent(vulnId);
99+
100+
if (config.proxySource) {
101+
url = `/triage/proxy?source=${encodeURIComponent(config.proxySource)}&id=${safeId}`;
102+
} else if (config.urlTemplate) {
103+
url = config.urlTemplate.replace("{id}", safeId);
104+
} else {
105+
throw new Error("Invalid configuration");
106+
}
107+
108+
const response = await fetch(url);
109+
if (!response.ok) {
110+
throw new Error(response.status === 404 ? "Not Found" : `Error: ${response.statusText}`);
111+
}
112+
return response.json();
113+
}
114+
115+
function updateColumn(column) {
116+
const select = column.querySelector(".source-select");
117+
const contentPre = column.querySelector(".json-content");
118+
const spinner = column.querySelector(".loading-spinner");
119+
const sourceKey = select.value;
120+
const vulnId = vulnIdInput.value.trim();
121+
122+
if (!sourceKey) {
123+
contentPre.textContent = "Select a source to view content";
124+
return;
125+
}
126+
127+
if (!vulnId) {
128+
contentPre.textContent = "Please enter a Vulnerability ID";
129+
return;
130+
}
131+
132+
spinner.classList.remove("hidden");
133+
contentPre.textContent = "";
134+
135+
fetchData(sourceKey, vulnId)
136+
.then((data) => {
137+
contentPre.innerHTML = syntaxHighlight(data);
138+
})
139+
.catch((error) => {
140+
contentPre.textContent = error.message;
141+
})
142+
.finally(() => {
143+
spinner.classList.add("hidden");
144+
});
145+
}
146+
147+
loadBtn.addEventListener("click", () => {
148+
columns.forEach((col) => updateColumn(col));
149+
});
150+
151+
// Also handle Enter key on the input field
152+
vulnIdInput.addEventListener("keydown", (e) => {
153+
if (e.key === "Enter") {
154+
columns.forEach((col) => updateColumn(col));
155+
}
156+
});
157+
158+
// Individual column updates when dropdown changes
159+
columns.forEach((col) => {
160+
const select = col.querySelector(".source-select");
161+
select.addEventListener("change", () => {
162+
if (vulnIdInput.value.trim()) {
163+
updateColumn(col);
164+
}
165+
});
166+
});
167+
});
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
.triage-container {
2+
// padding: 40px;
3+
padding: 20px 40px;
4+
width: 100%;
5+
box-sizing: border-box;
6+
7+
--md-sys-color-primary: #c5221f;
8+
--md-sys-color-on-primary: #ffffff;
9+
--md-filled-text-field-focus-active-indicator-color: #c5221f;
10+
--md-filled-text-field-focus-label-text-color: #c5221f;
11+
--md-filled-button-container-color: #c5221f;
12+
13+
.header-container {
14+
display: flex;
15+
justify-content: space-between;
16+
align-items: center;
17+
margin-bottom: 32px;
18+
}
19+
h1 {
20+
margin: 0;
21+
}
22+
23+
.input-section {
24+
display: flex;
25+
align-items: center;
26+
gap: 16px;
27+
28+
29+
.standard-input {
30+
flex: 1;
31+
max-width: 400px;
32+
}
33+
}
34+
}
35+
36+
.triage-grid {
37+
display: grid;
38+
grid-template-columns: repeat(3, 1fr);
39+
gap: 24px;
40+
}
41+
42+
.triage-column {
43+
border: 1px solid #555;
44+
border-radius: 8px;
45+
display: flex;
46+
flex-direction: column;
47+
height: 75vh;
48+
background-color: #333;
49+
}
50+
51+
.column-header {
52+
padding: 16px;
53+
background-color: #3c4043;
54+
border-bottom: 1px solid #555;
55+
border-top-left-radius: 8px;
56+
border-top-right-radius: 8px;
57+
58+
label {
59+
font-weight: bold;
60+
margin-bottom: 8px;
61+
display: block;
62+
}
63+
}
64+
65+
.select-wrapper {
66+
margin-top: 5px;
67+
}
68+
69+
.source-select {
70+
width: 100%;
71+
padding: 8px;
72+
background-color: #292929;
73+
color: #fff;
74+
border: 1px solid #555;
75+
border-radius: 4px;
76+
}
77+
78+
.content-display {
79+
flex-grow: 1;
80+
padding: 16px;
81+
overflow: auto;
82+
position: relative;
83+
background-color: #1e1e1e;
84+
}
85+
86+
.json-content {
87+
white-space: pre-wrap;
88+
word-wrap: break-word;
89+
font-family: "Overpass Mono", monospace;
90+
font-size: 13px;
91+
overflow-wrap: anywhere;
92+
margin: 0;
93+
color: #d4d4d4;
94+
95+
.json-key {
96+
color: #9cdcfe;
97+
}
98+
99+
.json-string {
100+
color: #ce9178;
101+
}
102+
103+
.json-number {
104+
color: #b5cea8;
105+
}
106+
107+
.json-boolean {
108+
color: #569cd6;
109+
}
110+
111+
.json-null {
112+
color: #569cd6;
113+
}
114+
}
115+
116+
.loading-spinner {
117+
position: absolute;
118+
top: 50%;
119+
left: 50%;
120+
transform: translate(-50%, -50%);
121+
--md-circular-progress-active-indicator-color: white;
122+
}
123+
124+
.hidden {
125+
display: none;
126+
}

gcp/website/frontend3/webpack.dev.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module.exports = {
99
entry: {
1010
main: './src/index.js',
1111
linter: './src/linter.js',
12+
triage: './src/triage.js',
1213
},
1314
output: {
1415
path: path.resolve(__dirname, '../dist'),
@@ -35,7 +36,7 @@ module.exports = {
3536
plugins: [
3637
new CopyPlugin({
3738
patterns: [
38-
{ from: './src/templates', to: '.', globOptions: { ignore: ['**/base.html'] } },
39+
{ from: './src/templates', to: '.', globOptions: { ignore: ['**/base.html', '**/triage.html'] } },
3940
{ from: './img/*', to: 'static/img/[name][ext]' },
4041
],
4142
}),
@@ -51,6 +52,12 @@ module.exports = {
5152
chunks: ['linter'],
5253
excludeChunks: ['main'],
5354
}),
55+
new HtmlWebpackPlugin({
56+
filename: 'triage.html',
57+
template: './src/templates/triage.html',
58+
chunks: ['triage'],
59+
excludeChunks: ['main', 'linter'],
60+
}),
5461
new MiniCssExtractPlugin({
5562
filename: 'static/[name].css'
5663
}),

0 commit comments

Comments
 (0)