Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cb00d98
feat: Add CVE conversion triage page
google-labs-jules[bot] Feb 24, 2026
623ea94
feat: Add CVE conversion triage page
google-labs-jules[bot] Feb 24, 2026
8025f2d
feat: Add CVE conversion triage page
google-labs-jules[bot] Feb 24, 2026
db6c422
feat: Add CVE conversion triage page
google-labs-jules[bot] Feb 24, 2026
95d87b5
Merge pull request #5 from jess-lowe/cve-conversion-triager-109754468…
jess-lowe Feb 25, 2026
48a23bd
Progress
jess-lowe Feb 25, 2026
2cf4f71
fix using path proxy and instead use source and id
jess-lowe Feb 25, 2026
82c8f83
fix overflow problem
jess-lowe Feb 25, 2026
1f46199
put title and vuln search inline
jess-lowe Feb 25, 2026
f95adc1
Merge pull request #6 from jess-lowe/cve-conversion-triager-109754468…
jess-lowe Feb 25, 2026
71067df
Update gcp/website/frontend3/src/templates/triage.html
jess-lowe Feb 26, 2026
49378ff
Update gcp/website/frontend3/src/templates/triage.html
jess-lowe Feb 26, 2026
f688808
Update gcp/website/frontend3/src/triage.scss
jess-lowe Feb 26, 2026
2560c7a
Update gcp/website/frontend3/src/triage.js
jess-lowe Feb 26, 2026
2e752b9
Update gcp/website/frontend3/src/triage.js
jess-lowe Feb 26, 2026
2fac31f
address nits
jess-lowe Feb 26, 2026
968d549
some improvements to reallly hopefully prevent xss
jess-lowe Feb 26, 2026
5af65c3
fix lint
jess-lowe Feb 27, 2026
231e3dd
Make sure this is only run locally for now
jess-lowe Feb 27, 2026
05a086d
lint
jess-lowe Feb 27, 2026
5d21790
Apply suggestion from @another-rex
jess-lowe Mar 2, 2026
df60885
bring back capture groups
jess-lowe Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions gcp/website/frontend3/src/templates/triage.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{% extends 'base.html' %}
{% set active_section = 'triage' %}

{% block content %}
<div class="triage-container">
<div class="header-container">
<h1>CVE Conversion Triager</h1>

<div class="input-section">
<md-filled-text-field id="vuln-id-input" label="Vulnerability ID" placeholder="e.g. CVE-2023-1234" class="standard-input"></md-filled-text-field>
<md-filled-button id="load-btn">Load</md-filled-button>
</div>
</div>
<div class="triage-grid">
{% for i in range(1, 4) %}
<div class="triage-column" id="column-{{ i }}">
<div class="column-header">
<label for="source-select-{{ i }}">Source:</label>
<div class="select-wrapper">
<select id="source-select-{{ i }}" class="source-select">
<option value="">Select Source...</option>
<optgroup label="External APIs">
<option value="cve-org">CVE.org API</option>
<option value="nvd-api">NVD API 2.0</option>
</optgroup>
<optgroup label="Test Instance (gs://osv-test-cve-osv-conversion)">
<option value="test-nvd">NVD-OSV (JSON)</option>
<option value="test-cve5">CVE5 (JSON)</option>
<option value="test-osv">OSV Output (JSON)</option>
<option value="test-nvd-metrics">NVD Metrics</option>
<option value="test-cve5-metrics">CVE5 Metrics</option>
</optgroup>
<optgroup label="Prod Instance (gs://cve-osv-conversion)">
<option value="prod-nvd">NVD-OSV (JSON)</option>
<option value="prod-cve5">CVE5 (JSON)</option>
<option value="prod-osv">OSV Output (JSON)</option>
<option value="prod-nvd-metrics">NVD Metrics</option>
<option value="prod-cve5-metrics">CVE5 Metrics</option>
</optgroup>
<optgroup label="API">
<option value="api-test">api.test.osv.dev</option>
<option value="api-prod">api.osv.dev</option>
</optgroup>
</select>
</div>
</div>
<div class="content-display">
<div class="loading-spinner hidden">
<md-circular-progress indeterminate></md-circular-progress>
</div>
<pre class="json-content">Select a source to view content</pre>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
167 changes: 167 additions & 0 deletions gcp/website/frontend3/src/triage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import "./triage.scss";
import "@material/web/textfield/filled-text-field.js";
import "@material/web/button/filled-button.js";
import "@material/web/progress/circular-progress.js";

document.addEventListener("DOMContentLoaded", () => {
const vulnIdInput = document.getElementById("vuln-id-input");
const loadBtn = document.getElementById("load-btn");
const columns = document.querySelectorAll(".triage-column");

// Map selection values to their respective endpoints/paths
const sourceConfigMap = {
// External APIs
"cve-org": {
proxySource: "cve",
},
"nvd-api": {
proxySource: "nvd",
},
// Test Instance
"test-nvd": {
proxySource: "test-nvd",
},
"test-cve5": {
proxySource: "test-cve5",
},
"test-osv": {
proxySource: "test-osv",
},
"test-nvd-metrics": {
proxySource: "test-nvd-metrics",
},
"test-cve5-metrics": {
proxySource: "test-cve5-metrics",
},
// Prod Instance
"prod-nvd": {
proxySource: "prod-nvd",
},
"prod-cve5": {
proxySource: "prod-cve5",
},
"prod-osv": {
proxySource: "prod-osv",
},
"prod-nvd-metrics": {
proxySource: "prod-nvd-metrics",
},
"prod-cve5-metrics": {
proxySource: "prod-cve5-metrics",
},
// API
"api-test": {
urlTemplate: "https://api.test.osv.dev/v1/vulns/{id}",
},
"api-prod": {
urlTemplate: "https://api.osv.dev/v1/vulns/{id}",
},
};

function escapeHtml(text) {
Comment thread
jess-lowe marked this conversation as resolved.
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

function syntaxHighlight(json) { // credit to https://codepen.io/absolutedevelopment/pen/EpwVzN
if (typeof json !== 'string') {
json = JSON.stringify(json, undefined, 2);
}

const escapedJson = escapeHtml(json);

return escapedJson.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
function (match) {
let cls = 'json-number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'json-key';
} else {
cls = 'json-string';
}
} else if (/true|false/.test(match)) {
cls = 'json-boolean';
} else if (/null/.test(match)) {
cls = 'json-null';
}
return `<span class="${cls}">${match}</span>`;
}
);
}

async function fetchData(sourceKey, vulnId) {
const config = sourceConfigMap[sourceKey];
let url;

const safeId = encodeURIComponent(vulnId);

if (config.proxySource) {
url = `/triage/proxy?source=${encodeURIComponent(config.proxySource)}&id=${safeId}`;
} else if (config.urlTemplate) {
url = config.urlTemplate.replace("{id}", safeId);
} else {
throw new Error("Invalid configuration");
}

const response = await fetch(url);
if (!response.ok) {
throw new Error(response.status === 404 ? "Not Found" : `Error: ${response.statusText}`);
}
return response.json();
}

function updateColumn(column) {
const select = column.querySelector(".source-select");
const contentPre = column.querySelector(".json-content");
const spinner = column.querySelector(".loading-spinner");
const sourceKey = select.value;
const vulnId = vulnIdInput.value.trim();

if (!sourceKey) {
contentPre.textContent = "Select a source to view content";
return;
}

if (!vulnId) {
contentPre.textContent = "Please enter a Vulnerability ID";
return;
}

spinner.classList.remove("hidden");
contentPre.textContent = "";

fetchData(sourceKey, vulnId)
.then((data) => {
contentPre.innerHTML = syntaxHighlight(data);
})
.catch((error) => {
contentPre.textContent = error.message;
})
.finally(() => {
spinner.classList.add("hidden");
});
}

loadBtn.addEventListener("click", () => {
columns.forEach((col) => updateColumn(col));
});

// Also handle Enter key on the input field
vulnIdInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
columns.forEach((col) => updateColumn(col));
}
});

// Individual column updates when dropdown changes
columns.forEach((col) => {
const select = col.querySelector(".source-select");
select.addEventListener("change", () => {
if (vulnIdInput.value.trim()) {
updateColumn(col);
}
});
});
});
126 changes: 126 additions & 0 deletions gcp/website/frontend3/src/triage.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
.triage-container {
// padding: 40px;
padding: 20px 40px;
width: 100%;
box-sizing: border-box;

--md-sys-color-primary: #c5221f;
--md-sys-color-on-primary: #ffffff;
--md-filled-text-field-focus-active-indicator-color: #c5221f;
--md-filled-text-field-focus-label-text-color: #c5221f;
--md-filled-button-container-color: #c5221f;

.header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
h1 {
margin: 0;
}

.input-section {
display: flex;
align-items: center;
gap: 16px;


.standard-input {
flex: 1;
max-width: 400px;
}
}
}

.triage-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}

.triage-column {
border: 1px solid #555;
border-radius: 8px;
display: flex;
flex-direction: column;
height: 75vh;
background-color: #333;
}

.column-header {
padding: 16px;
background-color: #3c4043;
border-bottom: 1px solid #555;
border-top-left-radius: 8px;
border-top-right-radius: 8px;

label {
font-weight: bold;
margin-bottom: 8px;
display: block;
}
}

.select-wrapper {
margin-top: 5px;
}

.source-select {
width: 100%;
padding: 8px;
background-color: #292929;
color: #fff;
border: 1px solid #555;
border-radius: 4px;
}

.content-display {
flex-grow: 1;
padding: 16px;
overflow: auto;
position: relative;
background-color: #1e1e1e;
}

.json-content {
white-space: pre-wrap;
word-wrap: break-word;
font-family: "Overpass Mono", monospace;
font-size: 13px;
overflow-wrap: anywhere;
margin: 0;
color: #d4d4d4;

.json-key {
color: #9cdcfe;
}

.json-string {
color: #ce9178;
}

.json-number {
color: #b5cea8;
}

.json-boolean {
color: #569cd6;
}

.json-null {
color: #569cd6;
}
}

.loading-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
--md-circular-progress-active-indicator-color: white;
}

.hidden {
display: none;
}
9 changes: 8 additions & 1 deletion gcp/website/frontend3/webpack.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {
entry: {
main: './src/index.js',
linter: './src/linter.js',
triage: './src/triage.js',
},
output: {
path: path.resolve(__dirname, '../dist'),
Expand All @@ -35,7 +36,7 @@ module.exports = {
plugins: [
new CopyPlugin({
patterns: [
{ from: './src/templates', to: '.', globOptions: { ignore: ['**/base.html'] } },
{ from: './src/templates', to: '.', globOptions: { ignore: ['**/base.html', '**/triage.html'] } },
{ from: './img/*', to: 'static/img/[name][ext]' },
],
}),
Expand All @@ -51,6 +52,12 @@ module.exports = {
chunks: ['linter'],
excludeChunks: ['main'],
}),
new HtmlWebpackPlugin({
filename: 'triage.html',
template: './src/templates/triage.html',
chunks: ['triage'],
excludeChunks: ['main', 'linter'],
}),
new MiniCssExtractPlugin({
filename: 'static/[name].css'
}),
Expand Down
Loading
Loading