From 7c19c11f4d71f8993d351a8a495064eb1f52df60 Mon Sep 17 00:00:00 2001 From: michaelkedar Date: Mon, 20 Jul 2026 03:38:51 +0000 Subject: [PATCH 1/6] Make a copy of html templates for go --- gcp/website/frontend3/src/go/base.html | 125 ++++ .../frontend3/src/go/templates/404.html | 54 ++ .../frontend3/src/go/templates/blog.html | 15 + .../frontend3/src/go/templates/blog_post.html | 15 + .../frontend3/src/go/templates/home.html | 349 ++++++++++ .../src/go/templates/linter/index.html | 98 +++ .../frontend3/src/go/templates/list.html | 166 +++++ .../frontend3/src/go/templates/triage.html | 68 ++ .../src/go/templates/vulnerability.html | 618 ++++++++++++++++++ gcp/website/frontend3/webpack.dev.js | 21 + gcp/website/frontend3/webpack.prod.js | 21 + 11 files changed, 1550 insertions(+) create mode 100644 gcp/website/frontend3/src/go/base.html create mode 100644 gcp/website/frontend3/src/go/templates/404.html create mode 100644 gcp/website/frontend3/src/go/templates/blog.html create mode 100644 gcp/website/frontend3/src/go/templates/blog_post.html create mode 100644 gcp/website/frontend3/src/go/templates/home.html create mode 100644 gcp/website/frontend3/src/go/templates/linter/index.html create mode 100644 gcp/website/frontend3/src/go/templates/list.html create mode 100644 gcp/website/frontend3/src/go/templates/triage.html create mode 100644 gcp/website/frontend3/src/go/templates/vulnerability.html diff --git a/gcp/website/frontend3/src/go/base.html b/gcp/website/frontend3/src/go/base.html new file mode 100644 index 00000000000..229c09a64c1 --- /dev/null +++ b/gcp/website/frontend3/src/go/base.html @@ -0,0 +1,125 @@ + + + + + + + + {% if disable_turbo_cache %} + + {% endif %} + + + + + + + + + + + OSV - Open Source Vulnerabilities + + + + {% block extra_head %}{% endblock %} + + + +
+ {% block top_bar %} +
+ + + + + + + +
    + + + + + + +
+
+ {% endblock %} + {% block content %}{% endblock %} +
+ + + diff --git a/gcp/website/frontend3/src/go/templates/404.html b/gcp/website/frontend3/src/go/templates/404.html new file mode 100644 index 00000000000..0a4265942c2 --- /dev/null +++ b/gcp/website/frontend3/src/go/templates/404.html @@ -0,0 +1,54 @@ +{% extends 'base.html' %} + +{% block extra_head %} + + + +{% endblock %} + +{% block top_bar %} {% endblock %} + +{% block content %} +{% if vuln_id %} +
+
+
+

{{ vuln_id }} Not Found!

+

+ {% if has_importfindings %} + The record has not been successfully imported from its source
+ {% endif %} + Please see our + FAQ for more information. +

+ +
+
+
+
+{% else %} +
+
+
+

Oops! Page Not Found!

+

+ While you're here, why not check out our + documentation or + FAQ? +

+ +
+
+
+
+{% endif %} +{% endblock %} diff --git a/gcp/website/frontend3/src/go/templates/blog.html b/gcp/website/frontend3/src/go/templates/blog.html new file mode 100644 index 00000000000..9c86dd302e7 --- /dev/null +++ b/gcp/website/frontend3/src/go/templates/blog.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% set active_section = 'blog' %} + +{% block content %} +
+
+
+
+

Blog

+ {{ index|safe }} +
+
+
+
+{% endblock %} diff --git a/gcp/website/frontend3/src/go/templates/blog_post.html b/gcp/website/frontend3/src/go/templates/blog_post.html new file mode 100644 index 00000000000..cd9e69b8c0a --- /dev/null +++ b/gcp/website/frontend3/src/go/templates/blog_post.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% set active_section = 'blog' %} +{% set disable_turbo_cache = 'true' %} + +{% block content %} +
+
+
+
+ {{ content|safe }} +
+
+
+
+{% endblock %} diff --git a/gcp/website/frontend3/src/go/templates/home.html b/gcp/website/frontend3/src/go/templates/home.html new file mode 100644 index 00000000000..85e0b588d8a --- /dev/null +++ b/gcp/website/frontend3/src/go/templates/home.html @@ -0,0 +1,349 @@ +{% extends 'base.html' %} +{% set active_section = 'home' %} + +{% block extra_head %} + + + + + + +{% endblock %} + +{% block content %} +
+
+
+

A distributed vulnerability database for Open Source

+
+

An open, precise, and distributed approach to producing and consuming vulnerability information for + open + source. +

+
+ + +
+
+

Ecosystems

+
+
+
+
+
+
+
+ {% if ecosystem_counts %} + {% set total = ecosystem_counts.values() | sum %} + {% for ecosystem, count in ecosystem_counts.items() %} + {% if count > 30 %} +
{{ ecosystem }}
+
+ {% set radius = [(count | log) / (total | log) * 100, 30] | max %} + + + {{ count }} + + View {{ ecosystem }} vulnerabilities + + + +
+ {% endif %} + {% endfor %} + {% endif %} +
+
+
+ +
+

OSV schema

+

+ All advisories in this database use the + OpenSSF OSV format, which + was developed in collaboration with open source communities. +

+

+ The OSV schema provides a human and machine readable data format to + describe vulnerabilities in a way that precisely maps to open source + package versions or commit hashes. +

+
+
+
{
+  "schema_version": "1.7.4",
+  "id": "GHSA-c3g4-w6cv-6v7h",
+  "modified": "2022-04-01T13:56:42Z",
+  "published": "2022-04-01T13:56:42Z",
+  "aliases": [ "CVE-2022-27651" ],
+  "summary": "Non-empty default inheritable capabilities for linux container in Buildah",
+  "details": "A bug was found in Buildah where containers were created ...",
+  "affected": [
+    {
+      "package": {
+        "ecosystem": "Go",
+        "name": "github.com/containers/buildah"
+      },
+      "ranges": [
+        {
+          "type": "SEMVER",
+          "events": [
+            {
+              "introduced": "0"
+            },
+            {
+              "fixed": "1.25.0"
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "references": [
+    {
+      "type": "WEB",
+      "url": "https://github.com/containers/buildah/commit/..."
+    },
+    {
+      "type": "PACKAGE",
+      "url": "https://github.com/containers/buildah"
+    }
+  ]
+}
+
+
+ +
+
+

Data sources

+

+ This infrastructure serves as an aggregator of vulnerability databases + that have adopted the OSV schema, including + GitHub Security Advisories, + PyPA, + RustSec, and + Global Security Database, and + more. +

+ +
+
+

Use the API

+

+ An easy-to-use API is available to query for all known vulnerabilities + by either a commit hash, or a package version. +

+
+
+

Query by commit hash

+
curl -d \
+  '{"commit": "6879efc2c1596d11a6a6ad296f80063b558d5e0f"}' \
+  "https://api.osv.dev/v1/query"
+ + + content_copy + + +
+
+

Query by version number

+
curl -d \
+  '{"version": "2.4.1",
+    "package": {"name": "jinja2", "ecosystem": "PyPI"}}' \
+  "https://api.osv.dev/v1/query"
+ + + content_copy + + +
+
+ +
+
+ +

Vulnerability Scanner

+
+
+
+

Install OSV‑Scanner

+
+go install github.com/google/osv-scanner/v2/cmd/osv-scanner@v2
+          
+ + + content_copy + + +
+
+
+

Scan SBOM or Lockfiles

+
+osv-scanner --sbom=cycloned-or-spdx-sbom.json
+osv-scanner --lockfile=package-lock.json
+          
+ + + content_copy + + +
+
+

Scan directory recursively

+
+osv-scanner -r path/to/your/project
+          
+ + + content_copy + + +
+
+
+ +
+
+
+ +

Remediation Tools

+
+
+

Guided Remediation (basic)

+
+osv-scanner fix --non-interactive --strategy=in-place -L path/to/package-lock.json
+osv-scanner fix --non-interactive --strategy=relock -M path/to/package.json -L path/to/package-lock.json
+          
+ + + content_copy + + +
+
+

Guided Remediation (interactive)

+
+osv-scanner fix -M path/to/package.json -L path/to/package-lock.json
+          
+ + + content_copy + + +
+ + +
+
+ +
+
+
+

Container Image Scanning

+

+ You can use + OSV-Scanner + to scan your container images for known vulnerabilities. +

+ + +
+
+
+

Scan container image

+
+osv-scanner scan image --serve alpine:3.12
+          
+ + + content_copy + + +
+
+
+ + +
+ Screenshot of container scan HTML output +
+ + +
+

GitHub Workflows

+

OSV-Scanner also provides reusable GitHub workflows that can be easily + integrated into CI/CD pipelines to provide continuous vulnerability scanning coverage. This can scan + newly added dependencies in pull requests for introduced vulnerabilities, as well as perform regular + vulnerability scans for the entire project.

+
Screenshot of OSV-Scanner GitHub Action
+ +
+ +
+

Open source

+

This project is open source. If you + have any ideas or questions, please feel free to reach out by creating an issue!

+ + +
+
+
+
+
+
{% endblock %} \ No newline at end of file diff --git a/gcp/website/frontend3/src/go/templates/linter/index.html b/gcp/website/frontend3/src/go/templates/linter/index.html new file mode 100644 index 00000000000..da68812b2a1 --- /dev/null +++ b/gcp/website/frontend3/src/go/templates/linter/index.html @@ -0,0 +1,98 @@ +{% set active_section = 'linter' %} {% block content %} + + + + + + + Vulnerability Linter Report + + + + + + + +
+
+
+ +
+

Open Source Vulnerabilities

+
+ sync +
+
+ +
+
+
+
+ Linter Report +
+
+ +
+
+ +
+
+
+
+ Home Database +
+

+
+ keyboard_arrow_down +
+
+
+
+
+ Finding +
+

+
+ keyboard_arrow_down +
+
+
+
+
+ + + +
+ + + + + + + + + + + +
Bug IDFindings + Modified unfold_more +
+ +
+
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/gcp/website/frontend3/src/go/templates/list.html b/gcp/website/frontend3/src/go/templates/list.html new file mode 100644 index 00000000000..77c2e20d99f --- /dev/null +++ b/gcp/website/frontend3/src/go/templates/list.html @@ -0,0 +1,166 @@ +{% extends 'base.html' %} +{% set active_section = 'vulnerabilities' %} +{% set disable_turbo_cache = 'true' %} + +{% macro table_header_cell(column_id, column_name, is_sortable, is_sorted, is_descending) %} + +
+
+ {{ column_name }} +
+ {% if is_sorted %} + + arrow_upward + + + {% endif %} +
+
+{% endmacro %} + +{% block content %} +
+
+
+
+

Vulnerabilities

+ +
+
+
+ +
+
+
+ {{ table_header_cell('id', 'ID', is_sortable=False, is_sorted=False, is_descending=False) }} + {{ table_header_cell('package', 'Packages', is_sortable=False, is_sorted=False, is_descending=False) }} + {{ table_header_cell('summary', 'Summary', is_sortable=False, is_sorted=False, is_descending=False) }} + {{ table_header_cell('published', 'Published', is_sortable=True, is_sorted=True, is_descending=True) }} + {{ table_header_cell('attributes', 'Attributes', is_sortable=False, is_sorted=False, is_descending=False) }} +
+
+
+ + {% for vulnerability in vulnerabilities %} +
+ + {{ vulnerability.id + }} + + +
    + {% for package in vulnerability.packages[:5] %} +
  • {{ package }}
  • + {% endfor %} + {% if vulnerability.packages|length > 5 %} + {% set remainingPkgCount = vulnerability.packages|length - 5 %} +
  • ... {{ remainingPkgCount }} more
  • + {% elif vulnerability.packages|length == 0 %} +
  • Not specified
  • + {% endif %} +
+
+ + {% if vulnerability.summary %} + {{ vulnerability.summary | literal_backticks }} + {% else %} + See record for full details + {% endif %} + + + + {{ vulnerability.published | relative_time }} + + + +
    +
  • + {%- if vulnerability.is_fixed -%} + Fix available + {%- else -%} + No fix available + {%- endif -%} +
  • + {%- if vulnerability.severity_score and vulnerability.severity_rating -%} +
  • + Severity - {{ + vulnerability.severity_score }} ({{ vulnerability.severity_rating }}) +
  • + {%- endif -%} +
+
+
+ {%- endfor -%} + {%- if vulnerabilities | length == 0 -%} + No results (check our FAQ if this is unexpected) + {%- endif -%} + {%- if page < total_pages -%} + + + {%- endif -%} +
+
+
+ + + +
+
+{% endblock %} diff --git a/gcp/website/frontend3/src/go/templates/triage.html b/gcp/website/frontend3/src/go/templates/triage.html new file mode 100644 index 00000000000..f031645bc47 --- /dev/null +++ b/gcp/website/frontend3/src/go/templates/triage.html @@ -0,0 +1,68 @@ +{% extends 'base.html' %} +{% set active_section = 'triage' %} + +{% block content %} +
+
+

CVE Conversion Triager

+ +
+ + Load +
+
+
+ {% for i in range(1, 4) %} +
+
+ +
+ +
+
+
+ +
+
Select a source to view content
+
+ + + + +
+
+ {% endfor %} +
+
+{% endblock %} diff --git a/gcp/website/frontend3/src/go/templates/vulnerability.html b/gcp/website/frontend3/src/go/templates/vulnerability.html new file mode 100644 index 00000000000..d1faee2eecb --- /dev/null +++ b/gcp/website/frontend3/src/go/templates/vulnerability.html @@ -0,0 +1,618 @@ +{% extends 'base.html' -%} +{% set active_section = 'vulnerabilities' -%} + +{% block extra_head %} + +{% endblock %} + +{% block content -%} +
+
+
+
+

+ {{ vulnerability.id }} +

+ {% if vulnerability.human_source_link and vulnerability.human_source_link.startswith("https://github.com/advisories/") -%} + + Suggest an improvement + + {% elif vulnerability.human_source_link and not vulnerability.id.startswith("openSUSE-") -%} + + {% else -%} + + See a problem? + + {% endif -%} +
+
+
+ {%- if vulnerability.human_source_link and not vulnerability.id.startswith("openSUSE-") -%} +
Source
+
{{ + vulnerability.human_source_link }} +
+ {%- endif -%} +
Import Source
+
{{ + vulnerability.source }}
+ +
JSON Data
+
+ https://{{ api_url }}/v1/vulns/{{ vulnerability.id }} +
+ {% if vulnerability.aliases -%} +
Aliases
+
+
    + {% for alias in vulnerability.aliases -%} +
  • + {% if alias in vulnerability.known_ids -%} + {{ alias }} + {% else -%} + {{ alias }} + {% endif -%} +
  • + {% endfor -%} +
+
+ {% endif -%} + {%- if vulnerability.upstream_hierarchy -%} +
Upstream
+
{{ vulnerability.upstream_hierarchy | safe }}
+ {%- endif -%} + {%- if vulnerability.downstream_hierarchy -%} +
Downstream
+
{{ vulnerability.downstream_hierarchy | safe }}
+ {%- endif -%} + {% if vulnerability.related -%} +
Related
+
+
    + {% for related in vulnerability.related -%} +
  • + {% if related in vulnerability.known_ids -%} + {{ related }} + {% else -%} + {{ related }} + {% endif -%} +
  • + {% endfor -%} +
+
+ {% endif -%} + {%- if vulnerability.withdrawn -%} +
+ Withdrawn + +
+
{{ vulnerability.withdrawn }}
+ {% endif -%} +
Published
+
{{ vulnerability.published }}
+
Modified
+
{{ vulnerability.modified }}
+ {%- if vulnerability.severity -%} +
Severity
+
+
    + {% for item in vulnerability.severity -%} +
  • + {% if item | is_cvss %} + {{ item | display_severity_rating }} + {{ item.type }} - {{ item.score }} + + CVSS Calculator + {% else %} + {{ item.type }} - {{ item.score }} + {% endif %} +
  • + {% endfor -%} +
+
+ {%- endif -%} +
Summary
+
+ {% if vulnerability.summary %} + {{ vulnerability.summary | literal_backticks }} + {% else %} + [none] + {% endif %} +
+
Details
+
+ {{ vulnerability.details | markdown | safe -}} +
+ {% if vulnerability.database_specific -%} +
+ Database specific + +
+
{{ vulnerability.database_specific | display_json }}
+ {% endif %} +
References
+
+ +
+ {% if vulnerability.credits -%} +
Credits
+
+
    + {% for credit in vulnerability.credits -%} +
  • +
      +
    • {{ credit.name }}{% if 'type' in credit %} - {{ credit.type }}{% endif %}
    • + {%- if 'contact' in credit -%} +
    • +
        + {%- for item in credit.contact -%} +
      • {{ item }}
      • + {%- endfor -%} +
      +
    • + {%- endif -%} +
    +
  • + {% endfor -%} +
+
+ {% endif %} +
+
+
+
+
+

Affected packages

+ + {% if vulnerability.affected|should_collapse %} + {% set ecosystems = vulnerability.affected | group_by_ecosystem %} +
+ {% for ecosystem_name, packages in ecosystems.items() -%} + {% set is_last_ecosystem = loop.last %} +
+ + {{ ecosystem_name }} + +
+ {% for affected in packages -%} +
+ + {% if 'package' in affected %} + {{ affected.package.name }} + {% else %} + {% set affected_repo = affected.ranges | default([], true) | selectattr('repo') | map(attribute='repo') | first %} + {% if affected_repo %} + {{ affected_repo | strip_scheme }} + {% endif %} + {% endif %} + +
+
+ {%- if 'package' in affected -%} +
+

Package

+
+
+
Name
+
{{ affected.package.name }}
+ {%- if ecosystem_name | has_link_to_deps_dev -%} +
View open source insights on deps.dev
+ {%- endif -%} + {%- if 'purl' in affected.package -%} +
Purl
+
{{ affected.package.purl }}
+ {%- endif -%} +
+
+
+ {%- endif -%} + {%- if 'severity' in affected -%} +
+

Severity

+
+
    + {% for item in affected.severity -%} +
  • + {% if item | is_cvss %} + {{ item | display_severity_rating }} + {{ item.type }} - {{ item.score }} + + CVSS Calculator + {% else %} + {{ item.type }} - {{ item.score }} + {% endif %} +
  • + {% endfor -%} +
+
+
+ {%- endif -%} +
+

Affected ranges

+
+ {% for range in affected.ranges -%} +
+
Type
+
{{ range.type -}}
+ {%- if range.repo -%} +
Repo
+
{{ range.repo }}
+ {%- endif -%} +
Events
+
+
+ {% for event in range.events -%} +
{{ event | event_type -}}
+
+ {% set link = event | event_link -%} + {% if link -%} + {{ event | event_value -}} + {% elif event | event_type == 'Introduced' and event | event_value == '0' -%} +
{{ event | event_value -}} + {% if range.type == 'GIT' %} + Unknown introduced commit / All previous commits are affected + {% else -%} + Unknown introduced version / All previous versions are affected + {% endif -%} +
+ {% else -%} + {{ event | event_value -}} + {% endif -%} +
+ {% endfor -%} +
+
+ {%- if range.database_specific -%} +
Database specific
+
{{ range.database_specific | display_json }}
+ {%- endif -%} +
+ {% endfor -%} +
+
+ {% if affected.versions -%} +
+

Affected versions

+
+ {% for group, versions in (affected.versions|group_versions(ecosystem_name)).items() -%} +
+ {{ group }} +
+ {% for version in versions -%} +
{{ version }}
+ {% endfor -%} +
+
+ {% endfor -%} +
+
+ {% endif -%} + {% if affected.ecosystem_specific -%} +
+

Ecosystem specific

+
{{ affected.ecosystem_specific | display_json }}
+
+ {% endif -%} + {% if affected.database_specific -%} +
+

Database specific

+
+ {% set db_specific = affected.database_specific %} + {% if db_specific is mapping %} +
+ {% for key, value in db_specific.items() %} +
+ {{ key }} +
+
{{ value | display_json }}
+
+
+ {% endfor %} +
+ {% else %} +
{{ db_specific | display_json }}
+ {% endif %} +
+
+ {% endif -%} +
+
+
+ {% endfor -%} +
+
+ {% endfor -%} +
+ + {% else %} + + {% for affected in vulnerability.affected -%} + {% if 'package' in affected %} + {% set ecosystem = affected.package.ecosystem %} + {% set package = affected.package.name %} + {% else %} + {% set ecosystem = 'Git' %} + {% set affected_repo = affected.ranges | default([], true) | selectattr('repo') | map(attribute='repo') | first %} + {% if affected_repo %} + {% set package = affected_repo | strip_scheme %} + {% endif %} + {% endif %} +

+ {{ ecosystem }} + / + {{ package }} +

+
+ {%- if 'package' in affected -%} +
+

+ Package +

+
+
+
Name
+ {%- if affected.package | package_in_ecosystem -%} +
{{ + affected.package.name }}
+ {%- else -%} +
{{ affected.package.name }}
+ {%- endif -%} + {%- if ecosystem | has_link_to_deps_dev -%} +
+ View open source insights on deps.dev
+ {%- endif -%} + {%- if 'purl' in affected.package -%} +
Purl
+
{{ affected.package.purl }}
+ {%- endif -%} +
+
+
+ {%- endif -%} + {%- if 'severity' in affected -%} +
+

+ Severity +

+
+
    + {% for item in affected.severity -%} +
  • + {% if item | is_cvss %} + {{ item | display_severity_rating }} + {{ item.type }} - {{ item.score }} + + CVSS Calculator + {% else %} + {{ item.type }} - {{ item.score }} + {% endif %} +
  • + {% endfor -%} +
+
+
+ {%- endif -%} +
+

+ Affected ranges + +

+
+ {% for range in affected.ranges -%} +
+
Type
+
{{ range.type -}}
+ + {%- if range.repo -%} +
Repo
+
{{ range.repo }}
+ {%- endif -%} + +
Events
+
+
+ {% for event in range.events -%} +
+ {{ event | event_type -}} +
+
+ {% set link = event | event_link -%} + {% if link -%} + + {{ event | event_value -}} + + {% elif event | event_type == 'Introduced' and event | event_value == '0' -%} +
+ {{ event | event_value -}} + {% if range.type == 'GIT' %} + Unknown introduced commit / All previous commits are affected + {% else -%} + Unknown introduced version / All previous versions are affected + {% endif -%} +
+ {% else -%} + {{ event | event_value -}} + {% endif -%} +
+ {% endfor -%} +
+
+ + {%- if range.database_specific -%} +
+ Database specific + +
+
{{ range.database_specific | display_json }}
+ {%- endif -%} +
+ {% endfor -%} +
+
+ {% if affected.versions -%} +
+

+ Affected versions + +

+
+ {% for group, versions in (affected.versions|group_versions(ecosystem)).items() -%} +
+ {{ group }} +
+ {% for version in versions -%} +
{{ version }}
+ {% endfor -%} +
+
+ {% endfor -%} +
+
+ {% endif -%} + {% if affected.ecosystem_specific -%} +
+

+ Ecosystem specific + +

+
+
{{ affected.ecosystem_specific | display_json }}
+
+
+ {% endif -%} + {% if affected.database_specific -%} +
+

+ Database specific + +

+
+ {% set db_specific = affected.database_specific %} + {% if db_specific is mapping %} +
+ {% for key, value in db_specific.items() %} +
+ {{ key }} +
+
{{ value | display_json }}
+
+
+ {% endfor %} +
+ {% else %} +
{{ db_specific | display_json }}
+ {% endif %} +
+
+ {% endif -%} +
+ {% endfor -%} +
+ {% endif %} +
+
+ + + + + +{% endblock -%} diff --git a/gcp/website/frontend3/webpack.dev.js b/gcp/website/frontend3/webpack.dev.js index b1523cbf340..0447f23fd8a 100644 --- a/gcp/website/frontend3/webpack.dev.js +++ b/gcp/website/frontend3/webpack.dev.js @@ -37,6 +37,8 @@ module.exports = { new CopyPlugin({ patterns: [ { from: './src/templates', to: '.', globOptions: { ignore: ['**/base.html', '**/triage.html'] } }, + // TODO(michaelkedar): Remove this once the website is fully migrated. + { from: './src/go/templates', to: 'go/', globOptions: { ignore: ['**/base.html', '**/triage.html'] } }, { from: './img/*', to: 'static/img/[name][ext]' }, ], }), @@ -58,6 +60,25 @@ module.exports = { chunks: ['triage'], excludeChunks: ['main', 'linter'], }), + // TODO(michaelkedar): Remove this once the website is fully migrated. + new HtmlWebpackPlugin({ + filename: 'go/base.html', + template: './src/go/base.html', + chunks: ['main'], + excludeChunks: ['linter'], + }), + new HtmlWebpackPlugin({ + filename: 'go/linter.html', + template: './src/go/templates/linter/index.html', + chunks: ['linter'], + excludeChunks: ['main'], + }), + new HtmlWebpackPlugin({ + filename: 'go/triage.html', + template: './src/go/templates/triage.html', + chunks: ['triage'], + excludeChunks: ['main', 'linter'], + }), new MiniCssExtractPlugin({ filename: 'static/[name].css' }), diff --git a/gcp/website/frontend3/webpack.prod.js b/gcp/website/frontend3/webpack.prod.js index 8c322a558c2..988beedb432 100644 --- a/gcp/website/frontend3/webpack.prod.js +++ b/gcp/website/frontend3/webpack.prod.js @@ -37,6 +37,8 @@ module.exports = { new CopyPlugin({ patterns: [ { from: './src/templates/*.html', to: '[name].html', globOptions: { ignore: ['**/base.html', '**/triage.html'] } }, + // TODO(michaelkedar): Remove this once the website is fully migrated. + { from: './src/go/templates/*.html', to: 'go/[name].html', globOptions: { ignore: ['**/base.html', '**/triage.html'] } }, { from: './img/*', to: 'static/img/[name][ext]' }, ], }), @@ -58,6 +60,25 @@ module.exports = { chunks: ['triage'], excludeChunks: ['main', 'linter'], }), + // TODO(michaelkedar): Remove this once the website is fully migrated. + new HtmlWebpackPlugin({ + filename: 'go/base.html', + template: './src/go/base.html', + chunks: ['main'], + excludeChunks: ['linter'], + }), + new HtmlWebpackPlugin({ + filename: 'go/linter.html', + template: './src/go/templates/linter/index.html', + chunks: ['linter'], + excludeChunks: ['main'], + }), + new HtmlWebpackPlugin({ + filename: 'go/triage.html', + template: './src/go/templates/triage.html', + chunks: ['triage'], + excludeChunks: ['main', 'linter'], + }), new MiniCssExtractPlugin({ filename: 'static/[name].[contenthash].css' }), From d5864e6e4abb9fbc302e890b6aeba4e4b16f9536 Mon Sep 17 00:00:00 2001 From: michaelkedar Date: Mon, 20 Jul 2026 04:13:01 +0000 Subject: [PATCH 2/6] base + home template --- gcp/website/frontend3/src/go/base.html | 39 ++++++----- .../frontend3/src/go/templates/home.html | 40 +++++------ go/internal/website/server.go | 40 ++++++++++- go/internal/website/server_test.go | 14 ++-- go/internal/website/static.go | 70 ++++++++++++++++++- 5 files changed, 152 insertions(+), 51 deletions(-) diff --git a/gcp/website/frontend3/src/go/base.html b/gcp/website/frontend3/src/go/base.html index 229c09a64c1..8f1a32cea86 100644 --- a/gcp/website/frontend3/src/go/base.html +++ b/gcp/website/frontend3/src/go/base.html @@ -6,9 +6,9 @@ content="Comprehensive vulnerability database for your open source projects and dependencies."> - {% if disable_turbo_cache %} + {{ if .DisableTurboCache }} - {% endif %} + {{ end }} - {% block extra_head %}{% endblock %} + {{ block "extra_head" . }}{{ end }} -
- {% block top_bar %} +
+ {{ block "top_bar" . }}
- {% endblock %} - {% block content %}{% endblock %} + {{ end }} + {{ block "content" . }}{{ end }}
+ diff --git a/gcp/website/frontend3/src/go/templates/home.html b/gcp/website/frontend3/src/go/templates/home.html index 85e0b588d8a..22b8a8ede35 100644 --- a/gcp/website/frontend3/src/go/templates/home.html +++ b/gcp/website/frontend3/src/go/templates/home.html @@ -1,7 +1,4 @@ -{% extends 'base.html' %} -{% set active_section = 'home' %} - -{% block extra_head %} +{{ define "extra_head" }} @@ -11,9 +8,9 @@ -{% endblock %} +{{ end }} -{% block content %} +{{ define "content" }}
@@ -25,7 +22,7 @@

A distributed vulnerability database for Open Source

- + search Search Vulnerability Database @@ -46,27 +43,21 @@

Ecosystems

- {% if ecosystem_counts %} - {% set total = ecosystem_counts.values() | sum %} - {% for ecosystem, count in ecosystem_counts.items() %} - {% if count > 30 %} -
{{ ecosystem }}
+ {{ range .Ecosystems }} +
{{ .Name }}
- {% set radius = [(count | log) / (total | log) * 100, 30] | max %} - + - {{ count }} + width: {{ .Radius }}px; height: {{ .Radius }}px; line-height: {{ .Radius }}px"> + {{ .Count }} - View {{ ecosystem }} vulnerabilities + top: {{ .TooltipTop }}px;"> + View {{ .Name }} vulnerabilities
- {% endif %} - {% endfor %} - {% endif %} + {{ end }}
@@ -333,9 +324,9 @@

GitHub Workflows

Open source

-

This project is open source. If you +

This project is open source. If you have any ideas or questions, please feel free to reach out by creating an issue!

+ href="https://github.com/google/osv.dev/issues/new/choose">creating an issue!

-
{% endblock %} \ No newline at end of file +
+{{ end }} \ No newline at end of file diff --git a/go/internal/website/server.go b/go/internal/website/server.go index b3b70fdcd41..0b491fd922f 100644 --- a/go/internal/website/server.go +++ b/go/internal/website/server.go @@ -2,11 +2,14 @@ package website import ( + "bytes" "errors" + "html/template" "io/fs" "log/slog" "net/http" "os" + "path" "time" "github.com/google/osv.dev/go/logger" @@ -16,8 +19,9 @@ const goVanityMetadata = `Home"), 0600); err != nil { + goDir := filepath.Join(tmpDir, "go") + if err := os.MkdirAll(goDir, 0755); err != nil { + t.Fatalf("failed to create go dir: %v", err) + } + if err := os.WriteFile(filepath.Join(goDir, "base.html"), []byte(`{{ block "content" . }}{{ end }}`), 0600); err != nil { + t.Fatalf("failed to write base.html: %v", err) + } + if err := os.WriteFile(filepath.Join(goDir, "home.html"), []byte(`{{ define "content" }}Home{{ end }}`), 0600); err != nil { t.Fatalf("failed to write home.html: %v", err) } if err := os.WriteFile(filepath.Join(imgDir, "favicon-32x32.png"), []byte("FAVICON"), 0600); err != nil { @@ -213,7 +220,7 @@ func TestStaticFiles(t *testing.T) { srv := newTestServer(t, website.Config{StaticFS: os.DirFS(tmpDir)}) - t.Run("Root serves home.html", func(t *testing.T) { + t.Run("Root", func(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() @@ -222,9 +229,6 @@ func TestStaticFiles(t *testing.T) { if rec.Code != http.StatusOK { t.Errorf("expected status 200 OK, got %d", rec.Code) } - if rec.Body.String() != "Home" { - t.Errorf("expected body Home, got %q", rec.Body.String()) - } }) t.Run("Favicon", func(t *testing.T) { diff --git a/go/internal/website/static.go b/go/internal/website/static.go index e4c75e9d7f8..eb2908fd13e 100644 --- a/go/internal/website/static.go +++ b/go/internal/website/static.go @@ -1,11 +1,72 @@ package website import ( + "context" "fmt" "io/fs" + "math" "net/http" + "sort" + "strings" ) +// EcosystemDisplay holds pre-calculated bubble data for ecosystem vulnerability counts on the home page. +type EcosystemDisplay struct { + Name string + Count int + Radius float64 + TooltipTop float64 +} + +// HomePageData represents the data context passed to home.html template. +type HomePageData struct { + ActiveSection string + DisableTurboCache bool + Ecosystems []EcosystemDisplay +} + +func computeEcosystemDisplays(counts map[string]int) []EcosystemDisplay { + var total int + for _, c := range counts { + total += c + } + if total == 0 { + return nil + } + + totalLog := math.Log(float64(total)) + displays := make([]EcosystemDisplay, 0, len(counts)) + for eco, count := range counts { + if count <= 30 { + continue + } + radius := math.Max((math.Log(float64(count))/totalLog)*100, 30) + tooltipTop := -((radius / 2) + 5) + displays = append(displays, EcosystemDisplay{ + Name: eco, + Count: count, + Radius: radius, + TooltipTop: tooltipTop, + }) + } + sort.Slice(displays, func(i, j int) bool { + return strings.ToLower(displays[i].Name) < strings.ToLower(displays[j].Name) + }) + + return displays +} + +func (s *Server) getEcosystemCounts(_ context.Context) map[string]int { + // Stub for ecosystem count fetch (Datastore or cache integration) + return map[string]int{ + "PyPI": 23174, + "npm": 222222, + "Go": 8010, + "GIT": 943411, + "Pub": 11, + } +} + func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("go-get") == "1" { w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -13,7 +74,14 @@ func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) { return } - http.ServeFileFS(w, r, s.config.StaticFS, "home.html") + + data := HomePageData{ + ActiveSection: "home", + DisableTurboCache: false, + Ecosystems: computeEcosystemDisplays(s.getEcosystemCounts(r.Context())), + } + + s.render(w, r, "home.html", http.StatusOK, data) } func (s *Server) handleGoBindingsVanity(w http.ResponseWriter, r *http.Request) { From b6452939109d34d0d3dda2c615ef593b5cd91e5d Mon Sep 17 00:00:00 2001 From: michaelkedar Date: Mon, 20 Jul 2026 05:37:37 +0000 Subject: [PATCH 3/6] 404 page (and some tidying) --- .../frontend3/src/go/templates/404.html | 29 +++++++++---------- go/internal/website/pagedata.go | 29 +++++++++++++++++++ go/internal/website/server.go | 17 +++++++++-- go/internal/website/server_test.go | 29 +++++++++++++++++-- go/internal/website/static.go | 29 +++++++++---------- 5 files changed, 98 insertions(+), 35 deletions(-) create mode 100644 go/internal/website/pagedata.go diff --git a/gcp/website/frontend3/src/go/templates/404.html b/gcp/website/frontend3/src/go/templates/404.html index 0a4265942c2..041cae32020 100644 --- a/gcp/website/frontend3/src/go/templates/404.html +++ b/gcp/website/frontend3/src/go/templates/404.html @@ -1,38 +1,34 @@ -{% extends 'base.html' %} - -{% block extra_head %} +{{ define "extra_head" }} -{% endblock %} +{{ end }} -{% block top_bar %} {% endblock %} +{{ define "top_bar" }}{{ end }} -{% block content %} -{% if vuln_id %} +{{ define "content" }} +{{ if .FailedImportVulnID }} -{% else %} +{{ else }}
@@ -43,12 +39,13 @@

Oops! Page Not Found!

FAQ?

-{% endif %} -{% endblock %} +{{ end }} +{{ end }} + diff --git a/go/internal/website/pagedata.go b/go/internal/website/pagedata.go new file mode 100644 index 00000000000..dab00c4b463 --- /dev/null +++ b/go/internal/website/pagedata.go @@ -0,0 +1,29 @@ +package website + +// BasePageData contains common fields required by the base.html layout template. +type BasePageData struct { + ActiveSection string + DisableTurboCache bool +} + +// EcosystemDisplay holds pre-calculated bubble data for ecosystem vulnerability counts on the home page. +type EcosystemDisplay struct { + Name string + Count int + Radius float64 + TooltipTop float64 +} + +// HomePageData represents the data context passed to home.html template. +type HomePageData struct { + BasePageData + + Ecosystems []EcosystemDisplay +} + +// NotFoundPageData represents the data context passed to 404.html template. +type NotFoundPageData struct { + BasePageData + + FailedImportVulnID string +} diff --git a/go/internal/website/server.go b/go/internal/website/server.go index 0b491fd922f..28114b73d30 100644 --- a/go/internal/website/server.go +++ b/go/internal/website/server.go @@ -69,8 +69,8 @@ func NewServer(cfg Config) (*Server, error) { } s.registerRoutes() - // Middlewares: Logging (if local/dev) -> ServeMux - var h http.Handler = s.mux + // Middlewares: 404 Fallback -> Logging (if local/dev) -> ServeMux + h := s.notFoundMiddleware(s.mux) // Skip HTTP access logging in Cloud Run production to avoid duplicating Cloud Run infrastructure logs. if os.Getenv("K_SERVICE") == "" { @@ -105,6 +105,19 @@ func loggingMiddleware(next http.Handler) http.Handler { }) } +// notFoundMiddleware returns an http.Handler that renders 404.html for unmatched routes. +func (s *Server) notFoundMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, pattern := s.mux.Handler(r) + if pattern == "" { + s.RenderNotFound(w, r, "") + + return + } + next.ServeHTTP(w, r) + }) +} + func (s *Server) registerRoutes() { // Health check s.mux.HandleFunc("GET /healthz", s.handleHealthz) diff --git a/go/internal/website/server_test.go b/go/internal/website/server_test.go index 7fb7bba809f..3cd699134e8 100644 --- a/go/internal/website/server_test.go +++ b/go/internal/website/server_test.go @@ -214,6 +214,9 @@ func TestStaticFiles(t *testing.T) { if err := os.WriteFile(filepath.Join(goDir, "home.html"), []byte(`{{ define "content" }}Home{{ end }}`), 0600); err != nil { t.Fatalf("failed to write home.html: %v", err) } + if err := os.WriteFile(filepath.Join(goDir, "404.html"), []byte(`{{ define "content" }}404 {{ .FailedImportVulnID }}{{ end }}`), 0600); err != nil { + t.Fatalf("failed to write 404.html: %v", err) + } if err := os.WriteFile(filepath.Join(imgDir, "favicon-32x32.png"), []byte("FAVICON"), 0600); err != nil { t.Fatalf("failed to write favicon: %v", err) } @@ -231,6 +234,28 @@ func TestStaticFiles(t *testing.T) { } }) + t.Run("renderNotFound", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(http.MethodGet, "/404", nil) + rec := httptest.NewRecorder() + srv.RenderNotFound(rec, req, "GHSA-1234") + + if rec.Code != http.StatusNotFound { + t.Errorf("expected status 404 Not Found, got %d", rec.Code) + } + }) + + t.Run("UnmatchedRoute_returns_404", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(http.MethodGet, "/foo/bar", nil) + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Errorf("expected status 404 Not Found, got %d", rec.Code) + } + }) + t.Run("Favicon", func(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodGet, "/favicon.ico", nil) @@ -300,8 +325,8 @@ func TestEndpointRegistration(t *testing.T) { rec := httptest.NewRecorder() srv.ServeHTTP(rec, req) - if rec.Code == http.StatusNotFound { - t.Errorf("expected route %s %s to be registered, got 404 Not Found", ep.method, ep.path) + if rec.Code != http.StatusNotImplemented { + t.Errorf("expected status 501 Not Implemented, got %d", rec.Code) } }) } diff --git a/go/internal/website/static.go b/go/internal/website/static.go index eb2908fd13e..241506bf8b9 100644 --- a/go/internal/website/static.go +++ b/go/internal/website/static.go @@ -10,19 +10,16 @@ import ( "strings" ) -// EcosystemDisplay holds pre-calculated bubble data for ecosystem vulnerability counts on the home page. -type EcosystemDisplay struct { - Name string - Count int - Radius float64 - TooltipTop float64 -} +func (s *Server) RenderNotFound(w http.ResponseWriter, r *http.Request, failedImportVulnID string) { + data := NotFoundPageData{ + BasePageData: BasePageData{ + ActiveSection: "", + DisableTurboCache: false, + }, + FailedImportVulnID: failedImportVulnID, + } -// HomePageData represents the data context passed to home.html template. -type HomePageData struct { - ActiveSection string - DisableTurboCache bool - Ecosystems []EcosystemDisplay + s.render(w, r, "404.html", http.StatusNotFound, data) } func computeEcosystemDisplays(counts map[string]int) []EcosystemDisplay { @@ -76,9 +73,11 @@ func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) { } data := HomePageData{ - ActiveSection: "home", - DisableTurboCache: false, - Ecosystems: computeEcosystemDisplays(s.getEcosystemCounts(r.Context())), + BasePageData: BasePageData{ + ActiveSection: "home", + DisableTurboCache: false, + }, + Ecosystems: computeEcosystemDisplays(s.getEcosystemCounts(r.Context())), } s.render(w, r, "home.html", http.StatusOK, data) From 538f540a4ec89b12026a16f92efd21cdefa8fc31 Mon Sep 17 00:00:00 2001 From: michaelkedar Date: Mon, 20 Jul 2026 06:03:20 +0000 Subject: [PATCH 4/6] blog --- .../frontend3/src/go/templates/blog.html | 9 +- .../frontend3/src/go/templates/blog_post.html | 10 +- go/internal/website/blog.go | 102 +++++++++++++++--- go/internal/website/pagedata.go | 16 +++ go/internal/website/server_test.go | 80 ++++++++++++-- 5 files changed, 183 insertions(+), 34 deletions(-) diff --git a/gcp/website/frontend3/src/go/templates/blog.html b/gcp/website/frontend3/src/go/templates/blog.html index 9c86dd302e7..f0c05304cc4 100644 --- a/gcp/website/frontend3/src/go/templates/blog.html +++ b/gcp/website/frontend3/src/go/templates/blog.html @@ -1,15 +1,12 @@ -{% extends 'base.html' %} -{% set active_section = 'blog' %} - -{% block content %} +{{ define "content" }}

Blog

- {{ index|safe }} + {{ .Index }}
-{% endblock %} +{{ end }} diff --git a/gcp/website/frontend3/src/go/templates/blog_post.html b/gcp/website/frontend3/src/go/templates/blog_post.html index cd9e69b8c0a..58a539c096b 100644 --- a/gcp/website/frontend3/src/go/templates/blog_post.html +++ b/gcp/website/frontend3/src/go/templates/blog_post.html @@ -1,15 +1,11 @@ -{% extends 'base.html' %} -{% set active_section = 'blog' %} -{% set disable_turbo_cache = 'true' %} - -{% block content %} +{{ define "content" }}
- {{ content|safe }} + {{ .Content }}
-{% endblock %} +{{ end }} diff --git a/go/internal/website/blog.go b/go/internal/website/blog.go index 7ca74004760..3e9b8ecb7bb 100644 --- a/go/internal/website/blog.go +++ b/go/internal/website/blog.go @@ -1,45 +1,117 @@ package website import ( - "fmt" + "html/template" + "io/fs" + "log/slog" + "mime" "net/http" + "path" + "regexp" + + "github.com/google/osv.dev/go/logger" ) +var validBlogName = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) + +func (s *Server) loadBlogContent(filePath string) (template.HTML, error) { + fullPath := path.Join("static", "blog", filePath) + content, err := fs.ReadFile(s.config.StaticFS, fullPath) + if err != nil { + return "", err + } + + return template.HTML(content), nil //nolint:gosec // Trusted static content built by Hugo +} + // handleBlogIndex handles serving the blog landing page /blog/. -func (s *Server) handleBlogIndex(w http.ResponseWriter, _ *http.Request) { - // TODO: Load blog index content from StaticFS (static/blog/index.html) and render blog template - http.Error(w, "Blog index handler stub", http.StatusNotImplemented) +func (s *Server) handleBlogIndex(w http.ResponseWriter, r *http.Request) { + indexHTML, err := s.loadBlogContent("index.html") + if err != nil { + s.RenderNotFound(w, r, "") + + return + } + + data := BlogPageData{ + BasePageData: BasePageData{ + ActiveSection: "blog", + DisableTurboCache: false, + }, + Index: indexHTML, + } + + s.render(w, r, "blog.html", http.StatusOK, data) } // handleBlogRSS handles serving the blog RSS feed /blog/index.xml. -func (s *Server) handleBlogRSS(w http.ResponseWriter, _ *http.Request) { - // TODO: Serve static/blog/index.xml from StaticFS - http.Error(w, "Blog RSS handler stub", http.StatusNotImplemented) +func (s *Server) handleBlogRSS(w http.ResponseWriter, r *http.Request) { + fullPath := path.Join("static", "blog", "index.xml") + rssContent, err := fs.ReadFile(s.config.StaticFS, fullPath) + if err != nil { + s.RenderNotFound(w, r, "") + + return + } + + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.WriteHeader(http.StatusOK) + if _, err := w.Write(rssContent); err != nil { + logger.ErrorContext(r.Context(), "Failed to write blog RSS response", slog.Any("error", err)) + } } // handleBlogPost handles serving individual blog posts /blog/posts/{blog_name}/. func (s *Server) handleBlogPost(w http.ResponseWriter, r *http.Request) { blogName := r.PathValue("blog_name") - if blogName == "" { - http.NotFound(w, r) + if blogName == "" || !validBlogName.MatchString(blogName) { + s.RenderNotFound(w, r, "") return } - // TODO: Validate blog_name, load static/blog/posts/{blog_name}/index.html and render post template - http.Error(w, fmt.Sprintf("Blog post handler stub (blogName=%q)", blogName), http.StatusNotImplemented) + postHTML, err := s.loadBlogContent(path.Join("posts", blogName, "index.html")) + if err != nil { + s.RenderNotFound(w, r, "") + + return + } + + data := BlogPostPageData{ + BasePageData: BasePageData{ + ActiveSection: "blog", + DisableTurboCache: true, + }, + Content: postHTML, + } + + s.render(w, r, "blog_post.html", http.StatusOK, data) } // handleBlogPostFile handles serving static assets inside blog post directories /blog/posts/{blog_name}/{file_name}. func (s *Server) handleBlogPostFile(w http.ResponseWriter, r *http.Request) { blogName := r.PathValue("blog_name") fileName := r.PathValue("file_name") - if blogName == "" || fileName == "" { - http.NotFound(w, r) + if blogName == "" || fileName == "" || !validBlogName.MatchString(blogName) { + s.RenderNotFound(w, r, "") return } - // TODO: Serve static/blog/posts/{blog_name}/{file_name} from StaticFS - http.Error(w, fmt.Sprintf("Blog post asset handler stub (blogName=%q, fileName=%q)", blogName, fileName), http.StatusNotImplemented) + assetPath := path.Join("static", "blog", "posts", blogName, path.Base(fileName)) + assetContent, err := fs.ReadFile(s.config.StaticFS, assetPath) + if err != nil { + s.RenderNotFound(w, r, "") + + return + } + + contentType := mime.TypeByExtension(path.Ext(fileName)) + if contentType != "" { + w.Header().Set("Content-Type", contentType) + } + w.WriteHeader(http.StatusOK) + if _, err := w.Write(assetContent); err != nil { + logger.ErrorContext(r.Context(), "Failed to write blog asset response", slog.String("asset", assetPath), slog.Any("error", err)) + } } diff --git a/go/internal/website/pagedata.go b/go/internal/website/pagedata.go index dab00c4b463..03e1259a3cd 100644 --- a/go/internal/website/pagedata.go +++ b/go/internal/website/pagedata.go @@ -1,5 +1,7 @@ package website +import "html/template" + // BasePageData contains common fields required by the base.html layout template. type BasePageData struct { ActiveSection string @@ -27,3 +29,17 @@ type NotFoundPageData struct { FailedImportVulnID string } + +// BlogPageData represents the data context passed to blog.html template. +type BlogPageData struct { + BasePageData + + Index template.HTML +} + +// BlogPostPageData represents the data context passed to blog_post.html template. +type BlogPostPageData struct { + BasePageData + + Content template.HTML +} diff --git a/go/internal/website/server_test.go b/go/internal/website/server_test.go index 3cd699134e8..8cc7114a3b1 100644 --- a/go/internal/website/server_test.go +++ b/go/internal/website/server_test.go @@ -221,8 +221,82 @@ func TestStaticFiles(t *testing.T) { t.Fatalf("failed to write favicon: %v", err) } + blogDir := filepath.Join(tmpDir, "static", "blog") + postDir := filepath.Join(blogDir, "posts", "hello-world") + if err := os.MkdirAll(postDir, 0755); err != nil { + t.Fatalf("failed to create blog post dir: %v", err) + } + if err := os.WriteFile(filepath.Join(goDir, "blog.html"), []byte(`{{ define "content" }}Blog Index: {{ .Index }}{{ end }}`), 0600); err != nil { + t.Fatalf("failed to write blog.html: %v", err) + } + if err := os.WriteFile(filepath.Join(goDir, "blog_post.html"), []byte(`{{ define "content" }}Blog Post: {{ .Content }}{{ end }}`), 0600); err != nil { + t.Fatalf("failed to write blog_post.html: %v", err) + } + if err := os.WriteFile(filepath.Join(blogDir, "index.html"), []byte("

Blog Index

"), 0600); err != nil { + t.Fatalf("failed to write blog index: %v", err) + } + if err := os.WriteFile(filepath.Join(blogDir, "index.xml"), []byte("RSS"), 0600); err != nil { + t.Fatalf("failed to write blog rss: %v", err) + } + if err := os.WriteFile(filepath.Join(postDir, "index.html"), []byte("
Hello World
"), 0600); err != nil { + t.Fatalf("failed to write blog post content: %v", err) + } + if err := os.WriteFile(filepath.Join(postDir, "hero.png"), []byte("PNG_DATA"), 0600); err != nil { + t.Fatalf("failed to write blog post image asset: %v", err) + } + srv := newTestServer(t, website.Config{StaticFS: os.DirFS(tmpDir)}) + t.Run("Blog_index", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(http.MethodGet, "/blog", nil) + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200 OK, got %d", rec.Code) + } + }) + + t.Run("Blog_rss_serves_xml", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(http.MethodGet, "/blog/index.xml", nil) + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200 OK, got %d", rec.Code) + } + if contentType := rec.Header().Get("Content-Type"); contentType != "application/xml; charset=utf-8" { + t.Errorf("expected content-type 'application/xml; charset=utf-8', got %q", contentType) + } + }) + + t.Run("Blog_post", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(http.MethodGet, "/blog/posts/hello-world/", nil) + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200 OK, got %d", rec.Code) + } + }) + + t.Run("Blog_post_asset_serves_file", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(http.MethodGet, "/blog/posts/hello-world/hero.png", nil) + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200 OK, got %d", rec.Code) + } + if rec.Body.String() != "PNG_DATA" { + t.Errorf("expected body 'PNG_DATA', got %q", rec.Body.String()) + } + }) + t.Run("Root", func(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodGet, "/", nil) @@ -294,12 +368,6 @@ func TestEndpointRegistration(t *testing.T) { method string path string }{ - {http.MethodGet, "/blog"}, - {http.MethodGet, "/blog/"}, - {http.MethodGet, "/blog/index.xml"}, - {http.MethodGet, "/blog/posts/test-post"}, - {http.MethodGet, "/blog/posts/test-post/"}, - {http.MethodGet, "/blog/posts/test-post/image.png"}, {http.MethodGet, "/vulnerability/GHSA-1234"}, {http.MethodGet, "/GHSA-1234"}, {http.MethodGet, "/vulnerability/GHSA-1234.json"}, From 4a49b9c7da5cdc0fd51eaa1f7765e1fefdbecf93 Mon Sep 17 00:00:00 2001 From: michaelkedar Date: Tue, 21 Jul 2026 00:08:39 +0000 Subject: [PATCH 5/6] osv linter --- .../src/go/templates/linter/index.html | 5 ++-- go/internal/website/linter.go | 23 ++++++++++++++----- go/internal/website/pagedata.go | 5 ++++ go/internal/website/server_test.go | 21 +++++++++++------ 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/gcp/website/frontend3/src/go/templates/linter/index.html b/gcp/website/frontend3/src/go/templates/linter/index.html index da68812b2a1..90f2721758f 100644 --- a/gcp/website/frontend3/src/go/templates/linter/index.html +++ b/gcp/website/frontend3/src/go/templates/linter/index.html @@ -1,4 +1,5 @@ -{% set active_section = 'linter' %} {% block content %} +{{ define "top_bar" }}{{ end }} +{{ define "content" }} @@ -95,4 +96,4 @@

Open Source Vulnerabilities

-{% endblock %} \ No newline at end of file +{{ end }} \ No newline at end of file diff --git a/go/internal/website/linter.go b/go/internal/website/linter.go index e31433bf2ed..d8536e843f9 100644 --- a/go/internal/website/linter.go +++ b/go/internal/website/linter.go @@ -1,21 +1,27 @@ package website import ( - "fmt" + "encoding/json" "net/http" ) // handleLinterPage handles serving the linter findings UI page. -func (s *Server) handleLinterPage(w http.ResponseWriter, _ *http.Request) { - // TODO: Serve linter UI page / template - http.Error(w, "Linter UI page stub", http.StatusNotImplemented) +func (s *Server) handleLinterPage(w http.ResponseWriter, r *http.Request) { + data := LinterPageData{ + BasePageData: BasePageData{ + ActiveSection: "linter", + }, + } + + s.render(w, r, "linter.html", http.StatusOK, data) } // handleLinterSources handles listing sources that have linter findings from GCS. func (s *Server) handleLinterSources(w http.ResponseWriter, _ *http.Request) { // TODO: List prefixes in GCS bucket osv-test-public-import-logs under linter-result/ w.Header().Set("Content-Type", "application/json") - http.Error(w, `{"error": "Linter sources handler stub"}`, http.StatusNotImplemented) + //nolint:errchkjson + _ = json.NewEncoder(w).Encode([]string{"ghsa", "cve-osv", "malicious-packages"}) } // handleLinterFindings handles fetching linter findings JSON for a specific source from GCS. @@ -29,5 +35,10 @@ func (s *Server) handleLinterFindings(w http.ResponseWriter, r *http.Request) { // TODO: Download linter-result//result.json from GCS bucket w.Header().Set("Content-Type", "application/json") - http.Error(w, fmt.Sprintf(`{"error": "Linter findings handler stub", "source": %q}`, source), http.StatusNotImplemented) + //nolint:errchkjson + _ = json.NewEncoder(w).Encode(map[string][]map[string]string{ + "path/OSV-1234-5678.json": {{ + "Code": "SCH:001", + "Message": "record is bad", + }}}) } diff --git a/go/internal/website/pagedata.go b/go/internal/website/pagedata.go index 03e1259a3cd..b983bcb5829 100644 --- a/go/internal/website/pagedata.go +++ b/go/internal/website/pagedata.go @@ -43,3 +43,8 @@ type BlogPostPageData struct { Content template.HTML } + +// LinterPageData represents the data context passed to linter.html template. +type LinterPageData struct { + BasePageData +} diff --git a/go/internal/website/server_test.go b/go/internal/website/server_test.go index 8cc7114a3b1..ccdd10fc352 100644 --- a/go/internal/website/server_test.go +++ b/go/internal/website/server_test.go @@ -244,9 +244,23 @@ func TestStaticFiles(t *testing.T) { if err := os.WriteFile(filepath.Join(postDir, "hero.png"), []byte("PNG_DATA"), 0600); err != nil { t.Fatalf("failed to write blog post image asset: %v", err) } + if err := os.WriteFile(filepath.Join(goDir, "linter.html"), []byte("Linter Report"), 0600); err != nil { + t.Fatalf("failed to write linter.html: %v", err) + } srv := newTestServer(t, website.Config{StaticFS: os.DirFS(tmpDir)}) + t.Run("Linter_page", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(http.MethodGet, "/linter", nil) + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200 OK, got %d", rec.Code) + } + }) + t.Run("Blog_index", func(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodGet, "/blog", nil) @@ -372,13 +386,6 @@ func TestEndpointRegistration(t *testing.T) { {http.MethodGet, "/GHSA-1234"}, {http.MethodGet, "/vulnerability/GHSA-1234.json"}, {http.MethodGet, "/GHSA-1234.json"}, - {http.MethodGet, "/list"}, - {http.MethodGet, "/api/search_suggestions"}, - {http.MethodGet, "/linter"}, - {http.MethodGet, "/linter/"}, - {http.MethodGet, "/linter-findings"}, - {http.MethodGet, "/linter-findings/"}, - {http.MethodGet, "/linter-findings/test-source"}, {http.MethodGet, "/triage"}, {http.MethodPost, "/triage/proxy"}, {http.MethodGet, "/login"}, From bd03595a2978fee8ca09aca5705c398a64319c1b Mon Sep 17 00:00:00 2001 From: michaelkedar Date: Tue, 21 Jul 2026 06:09:48 +0000 Subject: [PATCH 6/6] triage --- .../frontend3/src/go/templates/triage.html | 30 +++++++++++-------- go/internal/website/pagedata.go | 9 ++++++ go/internal/website/server.go | 1 + go/internal/website/server_test.go | 17 +++++++++-- go/internal/website/triage.go | 12 ++++++-- 5 files changed, 51 insertions(+), 18 deletions(-) diff --git a/gcp/website/frontend3/src/go/templates/triage.html b/gcp/website/frontend3/src/go/templates/triage.html index f031645bc47..49965c3ea00 100644 --- a/gcp/website/frontend3/src/go/templates/triage.html +++ b/gcp/website/frontend3/src/go/templates/triage.html @@ -1,7 +1,11 @@ -{% extends 'base.html' %} -{% set active_section = 'triage' %} +{{ define "extra_head" }} -{% block content %} + + + +{{ end }} + +{{ define "content" }}

CVE Conversion Triager

@@ -12,12 +16,12 @@

CVE Conversion Triager

- {% for i in range(1, 4) %} -
+ {{ range $i := .Columns }} +
- +
- @@ -51,18 +55,18 @@

CVE Conversion Triager

Select a source to view content
- - + aria-label="Copy JSON from column {{ $i }}" + title="Copy JSON from column {{ $i }}">
- {% endfor %} + {{ end }}
-{% endblock %} +{{ end }} diff --git a/go/internal/website/pagedata.go b/go/internal/website/pagedata.go index b983bcb5829..cb12bdbebe0 100644 --- a/go/internal/website/pagedata.go +++ b/go/internal/website/pagedata.go @@ -48,3 +48,12 @@ type BlogPostPageData struct { type LinterPageData struct { BasePageData } + +// TriagePageData represents the data context passed to triage.html template. +type TriagePageData struct { + BasePageData + + // Columns contains 1-indexed column numbers (e.g., []int{1, 2, 3}) to match + // element IDs (e.g. column-1) and URL query parameters (s1, s2, s3) expected by triage.js. + Columns []int +} diff --git a/go/internal/website/server.go b/go/internal/website/server.go index 28114b73d30..3de4ccc0876 100644 --- a/go/internal/website/server.go +++ b/go/internal/website/server.go @@ -170,6 +170,7 @@ func (s *Server) registerRoutes() { s.mux.HandleFunc("GET /linter-findings/{source}", s.handleLinterFindings) // Triage workflow + // TODO: auth stuff s.mux.HandleFunc("GET /triage", s.handleTriagePage) s.mux.HandleFunc("POST /triage/proxy", s.handleTriageProxy) diff --git a/go/internal/website/server_test.go b/go/internal/website/server_test.go index ccdd10fc352..b44e3d72fda 100644 --- a/go/internal/website/server_test.go +++ b/go/internal/website/server_test.go @@ -244,12 +244,26 @@ func TestStaticFiles(t *testing.T) { if err := os.WriteFile(filepath.Join(postDir, "hero.png"), []byte("PNG_DATA"), 0600); err != nil { t.Fatalf("failed to write blog post image asset: %v", err) } - if err := os.WriteFile(filepath.Join(goDir, "linter.html"), []byte("Linter Report"), 0600); err != nil { + if err := os.WriteFile(filepath.Join(goDir, "triage.html"), []byte(`{{ define "content" }}Triage{{ end }}`), 0600); err != nil { + t.Fatalf("failed to write triage.html: %v", err) + } + if err := os.WriteFile(filepath.Join(goDir, "linter.html"), []byte(`{{ define "content" }}Linter Report{{ end }}`), 0600); err != nil { t.Fatalf("failed to write linter.html: %v", err) } srv := newTestServer(t, website.Config{StaticFS: os.DirFS(tmpDir)}) + t.Run("Triage_page", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(http.MethodGet, "/triage", nil) + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200 OK, got %d", rec.Code) + } + }) + t.Run("Linter_page", func(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodGet, "/linter", nil) @@ -386,7 +400,6 @@ func TestEndpointRegistration(t *testing.T) { {http.MethodGet, "/GHSA-1234"}, {http.MethodGet, "/vulnerability/GHSA-1234.json"}, {http.MethodGet, "/GHSA-1234.json"}, - {http.MethodGet, "/triage"}, {http.MethodPost, "/triage/proxy"}, {http.MethodGet, "/login"}, {http.MethodGet, "/auth/callback"}, diff --git a/go/internal/website/triage.go b/go/internal/website/triage.go index ce8a0621e50..d32b2164a48 100644 --- a/go/internal/website/triage.go +++ b/go/internal/website/triage.go @@ -5,9 +5,15 @@ import ( ) // handleTriagePage handles serving the vulnerability triage UI page. -func (s *Server) handleTriagePage(w http.ResponseWriter, _ *http.Request) { - // TODO: Serve triage page / template - http.Error(w, "Triage UI page stub", http.StatusNotImplemented) +func (s *Server) handleTriagePage(w http.ResponseWriter, r *http.Request) { + data := TriagePageData{ + BasePageData: BasePageData{ + ActiveSection: "triage", + }, + Columns: []int{1, 2, 3}, + } + + s.render(w, r, "triage.html", http.StatusOK, data) } // handleTriageProxy handles proxying triage workflow actions.