diff --git a/AGENTS.md b/AGENTS.md
index 73ee03eff12..f4a9850e099 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -18,7 +18,8 @@ This repository (`google/osv.dev`) contains the backend services, database model
It is structured as a multi-language monorepo:
- `go/`: Go services and utilities (importers, exporter, internal libraries). **This is the primary target for active migrations from Python.**
- `osv/`: Core Python library containing models, repository helpers, and ecosystem-specific logic. *Note: Some parts are deprecated as we migrate logic to Go.*
-- `gcp/`: GCP deployment configurations, Cloud Functions, API server, and workers. (Website frontend uses `pnpm` and Hugo).
+- `gcp/`: GCP deployment configurations, Cloud Functions, and workers.
+- `website/`: Frontend assets for the osv.dev website (`frontend3` using `pnpm`) and blog posts (`blog` using Hugo).
- `vulnfeeds/`: Vulnerability feed utilities (*independent Go module*).
- `bindings/`: API bindings (*contains an independent Go module under `bindings/go`*).
@@ -73,7 +74,7 @@ The project uses `poetry` for Python dependency management, `pnpm` for website f
```
- **Install Website Dependencies** (for frontend development):
```bash
- cd gcp/website/frontend3 && pnpm install
+ cd website/frontend3 && pnpm install
```
- **Initialize Git Submodules**:
```bash
@@ -302,9 +303,9 @@ Contains deployment setups, workers running in GKE, Cloud Functions, and the use
- **Deployment Target**: **Google Cloud Run** (managed via Cloud Deploy pipeline `osv-api` deploying to `osv-grpc-backend`).
- *Note*: Fully migrated from Python to Go. Protobuf definitions and descriptor files are located under `proto/v1/`.
-### 2. Website (`go/cmd/website/`, `gcp/website/`)
-- **Status**: **Active (Go / Python)**.
-- Migrated to Go backend under `go/cmd/website/` and `go/internal/website/`. Frontend assets (Hugo blog, pnpm frontend3) are located under `website/` (symlinked to `gcp/website/`).
+### 2. Website (`go/cmd/website/`, `website/`)
+- **Status**: **Active (Go)**.
+- Fully migrated to Go backend under `go/cmd/website/` and `go/internal/website/`. Frontend assets (Hugo blog, pnpm frontend3) are located under `website/`.
- **Deployment Target**: **Google Cloud Run** (managed via Cloud Deploy pipeline `osv-website`).
### 3. Workers (`gcp/workers/`)
diff --git a/Makefile b/Makefile
index 6e805523d21..0634d065915 100644
--- a/Makefile
+++ b/Makefile
@@ -21,9 +21,6 @@ lib-tests: ## Run core Python library tests
vanir-signatures-tests: ## Run Vanir signatures tests
cd gcp/workers/vanir_signatures && ./run_tests.sh
-website-tests: ## Run legacy Python website tests
- cd gcp/website && ./run_tests.sh
-
vulnfeed-tests: ## Run Go vulnfeeds tests
cd vulnfeeds && ./run_tests.sh
@@ -118,7 +115,7 @@ run-api-server-test:
@cd go && go build -o ./api-devserver ./cmd/api-devserver && (GOOGLE_CLOUD_PROJECT=oss-vdb-test OSV_VULNERABILITIES_BUCKET=osv-test-vulnerabilities ./api-devserver $(ARGS); EXIT_CODE=$$?; rm -f ./api-devserver; exit $$EXIT_CODE)
# TODO: API integration tests.
-all-tests: lib-tests website-tests vulnfeed-tests bindings-tests go-tests ## Run all tests
+all-tests: lib-tests vulnfeed-tests bindings-tests go-tests ## Run all tests
reimport-tui: ## Run the reimport TUI tool
test -f $(HOME)/.config/gcloud/application_default_credentials.json || (echo "GCP Application Default Credentials not set, try 'gcloud auth application-default login'"; exit 1)
diff --git a/README.md b/README.md
index 3a87932e5e4..f2be6830541 100644
--- a/README.md
+++ b/README.md
@@ -43,12 +43,12 @@ consists of:
| `gcp/datastore` | The datastore index file (`index.yaml`) |
| `gcp/functions` | The Cloud Function for publishing PyPI vulnerabilities (maintained, but not developed) |
| `gcp/indexer` | The determine version `indexer` |
-| `gcp/website` | Frontend assets for the osv.dev website (in `frontend3`) and blog posts (in `blog`) |
| `gcp/workers/` | Python workers (`vanir_signatures` and `oss_fuzz_worker`) |
| `go/` | Go module for shared libraries and commands (`cmd/api`, `cmd/website`, `cmd/importer`, `cmd/worker`, `cmd/exporter`, `cmd/recoverer`, `cmd/relations`, etc.) |
| `osv/` | The core OSV Python library, used in basically all Python services OSV ecosystem package versioning helpers in `ecosystems/` Datastore model definitions in `models.py` |
| `tools/` | Misc scripts/tools, mostly intended for development (datastore stuff, linting) The `indexer-api-caller` for indexer calling |
| `vulnfeeds/` | Go module for (mostly) the NVD CVE conversion The Alpine feed converter (`cmd/alpine`) The Debian feed converter (`tools/debian`, which is written in Python) |
+| `website/` | Frontend assets for the osv.dev website (in `frontend3`) and blog posts (in `blog`) |
You'll need to check out submodules as well for many local building steps to
diff --git a/cloudbuild.yaml b/cloudbuild.yaml
index dd82e954428..dd55a7453b4 100644
--- a/cloudbuild.yaml
+++ b/cloudbuild.yaml
@@ -27,11 +27,6 @@ steps:
args: ['gcloud', 'builds', 'submit', '--region=${LOCATION}', '--config=gcp/workers/cloudbuild.yaml', '.']
waitFor: ['init']
-- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
- id: 'website-tests'
- args: ['gcloud', 'builds', 'submit', '--region=${LOCATION}', '--config=gcp/website/cloudbuild.yaml', '.']
- waitFor: ['init']
-
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
id: 'vulnfeeds-tests'
args: ['gcloud', 'builds', 'submit', '--region=${LOCATION}', '--config=vulnfeeds/cloudbuild.yaml', '.']
diff --git a/deployment/build-and-stage.yaml b/deployment/build-and-stage.yaml
index ff14825c64e..7adcc171682 100644
--- a/deployment/build-and-stage.yaml
+++ b/deployment/build-and-stage.yaml
@@ -462,13 +462,13 @@ steps:
corepack enable pnpm
pnpm install --frozen-lockfile --ignore-scripts
pnpm run build:prod
- dir: 'gcp/website/frontend3'
+ dir: 'website/frontend3'
id: 'build-frontend3'
waitFor: ['setup']
- name: 'gcr.io/oss-vdb/ci'
args: ['hugo', '--buildFuture', '-d', '../dist/static/blog']
- dir: 'gcp/website/blog'
+ dir: 'website/blog'
id: 'build-hugo'
waitFor: ['setup']
@@ -483,7 +483,7 @@ steps:
'-t', 'gcr.io/oss-vdb/osv-website:latest', '-t', 'gcr.io/oss-vdb/osv-website:$COMMIT_SHA',
'--target', 'website',
'--build-context', 'bindings=../bindings',
- '--build-context', 'website-dist=../gcp/website/dist',
+ '--build-context', 'website-dist=../website/dist',
'--build-context', 'docs=../docs',
'-f', 'Dockerfile', '--cache-from', 'gcr.io/oss-vdb/osv-website:latest', '--pull', '.']
dir: 'go'
diff --git a/docs/contributing/architecture.md b/docs/contributing/architecture.md
index 74250d74ab6..dd3230c79fd 100644
--- a/docs/contributing/architecture.md
+++ b/docs/contributing/architecture.md
@@ -67,7 +67,6 @@ The [API server](../api/index.md) (hosted at `api.osv.dev`, source code in [`go/
## Website
-The [main web UI](https://osv.dev) (source code in [`gcp/website`](../../gcp/website)) also runs on [Cloud Run], and is served through [Cloud Load Balancing].
+The [main web UI](https://osv.dev) (server source code in [`go/cmd/website`](../../go/cmd/website) and frontend assets in [`website`](../../website)) also runs on [Cloud Run], and is served through [Cloud Load Balancing].
-[Cloud Run]: https://cloud.google.com/run
[Cloud Load Balancing]: https://cloud.google.com/load-balancing
diff --git a/gcp/website/.gitignore b/gcp/website/.gitignore
deleted file mode 100644
index 8e42b8e16ac..00000000000
--- a/gcp/website/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-dist/
-requirements.txt
-app.yaml
-cron-service.yaml
-testing-app.yaml
diff --git a/gcp/website/Dockerfile b/gcp/website/Dockerfile
deleted file mode 100644
index 7374bbe4c5d..00000000000
--- a/gcp/website/Dockerfile
+++ /dev/null
@@ -1,69 +0,0 @@
-# Build the Javascript frontend
-FROM node:24.20@sha256:be23f54a88d34e8824c741b19b91064094f92c1c97b194144bfc8b50d67258e2 AS FRONTEND3_BUILD
-WORKDIR /build/frontend3
-
-# Install dependencies first for better caching
-RUN corepack enable pnpm
-COPY gcp/website/frontend3/package.json gcp/website/frontend3/pnpm-lock.yaml ./
-RUN pnpm install --frozen-lockfile --ignore-scripts
-
-COPY gcp/website/frontend3/webpack.prod.js ./
-COPY gcp/website/frontend3/img img
-COPY gcp/website/frontend3/src src
-
-RUN pnpm run build:prod
-
-# Build hugo blogs
-# Use the ci image, since it already built the version of hugo we want from source
-FROM gcr.io/oss-vdb/ci AS HUGO_BUILD
-
-WORKDIR /build/blog
-COPY gcp/website/blog ./
-
-RUN hugo --buildFuture -d ../dist/static/blog
-
-# OSV.dev site image
-# Adapted from https://cloud.google.com/run/docs/quickstarts/build-and-deploy/deploy-python-service#writing
-FROM python:3.13.3-slim@sha256:56a11364ffe0fee3bd60af6d6d5209eba8a99c2c16dc4c7c5861dc06261503cc
-
-# Generation 1 of cloud run overrides the HOME environment variable, causing
-# poetry to run in the incorrect environment, as it defaults to using $HOME/.cache/virtualenvs/...
-#
-# This forces it to create the virtualenv in the same directory as the project, avoiding this issue.
-ENV POETRY_VIRTUALENVS_IN_PROJECT=true
-ENV POETRY_HOME "/opt/poetry"
-COPY docker/poetry/requirements.txt ./poetry-requirements.txt
-RUN python3 -m venv $POETRY_HOME && \
- $POETRY_HOME/bin/pip install --require-hashes -r ./poetry-requirements.txt && \
- ln -s $POETRY_HOME/bin/poetry /usr/local/bin/poetry
-
-# Allow statements and log messages to immediately appear in the logs
-ENV PYTHONUNBUFFERED True
-WORKDIR /osv/gcp/website
-
-# Install Python dependencies
-COPY poetry.lock pyproject.toml README.md /osv/
-COPY osv /osv/osv
-COPY gcp/website/poetry.lock gcp/website/pyproject.toml ./
-RUN poetry install
-
-# Website Python code
-COPY gcp/website/*.py ./
-
-# JS/hugo builds
-COPY gcp/website/dist/public_keys dist/public_keys
-COPY gcp/website/docs docs
-# gcp/website/docs/docs/osv_service_v1.swagger.json is a symlink
-COPY docs/osv_service_v1.swagger.json docs/
-
-COPY --from=FRONTEND3_BUILD /build/dist/ dist/
-COPY --from=HUGO_BUILD /build/dist dist/
-
-RUN poetry run python -m whitenoise.compress dist/static/
-
-# Run the web service on container startup. Here we use the gunicorn
-# webserver, with one worker process and 8 threads.
-# For environments with multiple CPU cores, increase the number of workers
-# to be equal to the cores available.
-# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling.
-CMD poetry run gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
diff --git a/gcp/website/auth.py b/gcp/website/auth.py
deleted file mode 100644
index 1bda4730ad0..00000000000
--- a/gcp/website/auth.py
+++ /dev/null
@@ -1,138 +0,0 @@
-"""Authentication handlers."""
-import os
-import secrets
-from functools import wraps
-import requests
-
-from flask import Blueprint, request, session, redirect, url_for, jsonify
-from google.oauth2 import id_token
-from google.auth.transport import requests as google_requests
-
-blueprint = Blueprint('auth', __name__)
-
-GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_OAUTH_CLIENT_ID")
-GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET")
-GOOGLE_DISCOVERY_URL = ("https://accounts.google.com/"
- ".well-known/openid-configuration")
-
-# Easy bypass for local development testing
-BYPASS_OAUTH_FOR_LOCAL_DEV = os.environ.get("BYPASS_OAUTH_FOR_LOCAL_DEV",
- "False").lower() in ("true", "1",
- "t")
-
-
-def get_google_provider_cfg():
- """Get Google provider configuration."""
- return requests.get(GOOGLE_DISCOVERY_URL, timeout=10).json()
-
-
-@blueprint.route("/login")
-def login():
- """Login route."""
- if BYPASS_OAUTH_FOR_LOCAL_DEV:
- session['user_email'] = 'dev@google.com'
- return redirect(url_for('triage_handlers.triage_index'))
-
- if not GOOGLE_CLIENT_ID or not GOOGLE_CLIENT_SECRET:
- return jsonify({
- 'error': 'OAuth credentials not configured. '
- 'Set GOOGLE_OAUTH_CLIENT_ID and '
- 'GOOGLE_OAUTH_CLIENT_SECRET env vars or '
- 'enable BYPASS_OAUTH_FOR_LOCAL_DEV.'
- }), 500
-
- google_provider_cfg = get_google_provider_cfg()
- authorization_endpoint = google_provider_cfg["authorization_endpoint"]
-
- state = secrets.token_urlsafe(16)
- session['oauth_state'] = state
-
- redirect_uri = url_for('auth.callback', _external=True)
- # Ensure redirect_uri uses https if the app is accessed over https
- # (for environments behind load balancers without proxyfix)
- if request.headers.get(
- 'X-Forwarded-Proto',
- 'http') == 'https' and redirect_uri.startswith('http://'):
- redirect_uri = redirect_uri.replace('http://', 'https://', 1)
-
- request_uri = (f"{authorization_endpoint}?response_type=code"
- f"&client_id={GOOGLE_CLIENT_ID}"
- f"&redirect_uri={redirect_uri}"
- f"&scope=openid%20email%20profile"
- f"&state={state}"
- f"&access_type=offline")
-
- return redirect(request_uri)
-
-
-@blueprint.route("/auth/callback")
-def callback():
- """Auth callback route."""
- if request.args.get('state') != session.get('oauth_state'):
- return jsonify({'error': 'Invalid state parameter'}), 400
-
- code = request.args.get("code")
- google_provider_cfg = get_google_provider_cfg()
- token_endpoint = google_provider_cfg["token_endpoint"]
-
- redirect_uri = url_for('auth.callback', _external=True)
- if request.headers.get(
- 'X-Forwarded-Proto',
- 'http') == 'https' and redirect_uri.startswith('http://'):
- redirect_uri = redirect_uri.replace('http://', 'https://', 1)
-
- token_url = token_endpoint
- token_data = {
- "code": code,
- "client_id": GOOGLE_CLIENT_ID,
- "client_secret": GOOGLE_CLIENT_SECRET,
- "redirect_uri": redirect_uri,
- "grant_type": "authorization_code",
- }
-
- token_response = requests.post(token_url, data=token_data, timeout=10)
- token_json = token_response.json()
-
- if "id_token" not in token_json:
- return jsonify({
- 'error': 'Failed to obtain ID token. '
- 'Maybe credentials mismatch or invalid code.'
- }), 400
-
- token = token_json["id_token"]
-
- try:
- # Verify the token
- client_request = google_requests.Request()
- id_info = id_token.verify_oauth2_token(token, client_request,
- GOOGLE_CLIENT_ID)
-
- # The oauth only allows users to login with accounts that have
- # access to the GCP project
- session['user_email'] = id_info.get("email")
- return redirect(url_for('triage_handlers.triage_index'))
-
- except ValueError:
- return jsonify({'error': 'Invalid token'}), 400
-
-
-@blueprint.route("/logout")
-def logout():
- session.pop('user_email', None)
- return redirect(url_for('triage_handlers.triage_index'))
-
-
-def require_google_account(f):
- """Decorator to require Google account."""
-
- @wraps(f)
- def decorated_function(*args, **kwargs):
- if BYPASS_OAUTH_FOR_LOCAL_DEV:
- return f(*args, **kwargs)
-
- if 'user_email' not in session:
- return redirect(url_for('auth.login'))
-
- return f(*args, **kwargs)
-
- return decorated_function
diff --git a/gcp/website/build.sh b/gcp/website/build.sh
deleted file mode 100755
index afa18998a6c..00000000000
--- a/gcp/website/build.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/bash -x
-# Copyright 2024 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-cd ../.. &&
- docker build -t gcr.io/oss-vdb/osv-website:$1 -t gcr.io/oss-vdb/osv-website:latest -f gcp/website/Dockerfile --pull .
diff --git a/gcp/website/cache.py b/gcp/website/cache.py
deleted file mode 100644
index 95c22b0ac65..00000000000
--- a/gcp/website/cache.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# Copyright 2022 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""Caching."""
-
-import functools
-import os
-from typing import Callable
-from concurrent.futures import ThreadPoolExecutor, Future
-
-import flask_caching
-
-import utils
-
-if utils.is_cloud_run():
- instance = flask_caching.Cache(
- config={
- 'CACHE_TYPE': 'RedisCache',
- 'CACHE_REDIS_HOST': os.environ.get('REDISHOST', '127.0.0.1'),
- 'CACHE_REDIS_PORT': int(os.environ.get('REDISPORT', 6379)),
- })
-else:
- instance = flask_caching.Cache(config={
- 'CACHE_TYPE': 'SimpleCache',
- })
-
-_should_refresh_suffix = '__refresh_marker'
-_executor_map: dict[str, ThreadPoolExecutor] = {}
-_future_map: dict[str, Future] = {}
-
-
-def smart_cache(
- key: str,
- hard_timeout: int,
- soft_timeout: int,
-) -> Callable:
- """
- The decorated function will be cached with the given key.
-
- If the decorated function is called any time after the `soft_timeout`
- of the cache, the cached value will still be returned, but the cache
- will be refreshed in an asynchronous background thread.
-
- Currently this decorator does not support differing arguments.
- """
- if key in _executor_map:
- raise ValueError('key already exists')
-
- # Only require one background thread to run per cache key
- # since we check whether an existing update task is already running
- # before queuing another update.
- _executor_map[key] = ThreadPoolExecutor(1)
-
- def decorator(f):
-
- @functools.wraps(f)
- def decorated_function(*args, **kwargs):
-
- def update_func():
- result = f(*args, **kwargs)
- instance.set(key, result, timeout=hard_timeout)
- instance.set(key + _should_refresh_suffix, True, timeout=soft_timeout)
- return result
-
- result = instance.get(key)
- if result is None: # If the main cached value expires, refresh normally
- return update_func()
-
- # Only refresh the cached value once instance times out
- should_refresh_cached_value = not instance.has(key +
- _should_refresh_suffix)
- future = _future_map.get(key)
- if should_refresh_cached_value and (future is None or future.done()):
- # Store the future to make sure it executes.
- # (Dropped futures are not executed)
- # Also for reference to prevent queueing up multiple updates
- _future_map[key] = _executor_map[key].submit(update_func)
-
- return result
-
- return decorated_function
-
- return decorator
diff --git a/gcp/website/cloudbuild.yaml b/gcp/website/cloudbuild.yaml
deleted file mode 100644
index 4fd73299be1..00000000000
--- a/gcp/website/cloudbuild.yaml
+++ /dev/null
@@ -1,49 +0,0 @@
-# Copyright 2025 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# website test runner
-# This should be triggered on changes to the following:
-# gcp/website/poetry.lock
-# gcp/website/run_tests.sh
-# gcp/website/*.py
-# gcp/website/**/*.py
-# osv/*.py
-# osv/**/*.py
-
-steps:
-- name: 'gcr.io/cloud-builders/git'
- id: 'init'
- args: ['submodule', 'update', '--init']
- # if this is invoked from another cloud build, this will fail as it is not a git repo
- # the invoking cloud build file should run this step.
- allowFailure: true
-- name: 'gcr.io/oss-vdb/ci'
- id: 'sync'
- dir: gcp/website
- args: ['poetry', 'sync']
- waitFor: ['-']
-
-- name: 'gcr.io/oss-vdb/ci'
- id: 'website-tests'
- dir: gcp/website
- args: ['bash', '-ex', 'run_tests.sh']
- env:
- # Each concurrent test that uses the datastore emulator must have a unique port number
- - DATASTORE_EMULATOR_PORT=8004
- waitFor: ['init', 'sync']
-
-timeout: 7200s
-options:
- env:
- - CLOUDBUILD=1
diff --git a/gcp/website/docs/osv_service_v1.swagger.json b/gcp/website/docs/osv_service_v1.swagger.json
deleted file mode 120000
index 3666a5d9c51..00000000000
--- a/gcp/website/docs/osv_service_v1.swagger.json
+++ /dev/null
@@ -1 +0,0 @@
-../../../docs/osv_service_v1.swagger.json
\ No newline at end of file
diff --git a/gcp/website/emulator_aliases.py b/gcp/website/emulator_aliases.py
deleted file mode 100644
index 1766fd76848..00000000000
--- a/gcp/website/emulator_aliases.py
+++ /dev/null
@@ -1,203 +0,0 @@
-# Copyright 2023 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""OSV alias computation."""
-import datetime
-import logging
-
-from google.cloud import exceptions
-from google.cloud import ndb
-
-import osv
-from osv import gcs, pubsub
-
-ALIAS_GROUP_VULN_LIMIT = 32
-VULN_ALIASES_LIMIT = 5
-
-
-def _update_group(bug_ids: list[str], alias_group: osv.AliasGroup,
- changed_vulns: dict[str, osv.AliasGroup | None]):
- """Updates the alias group in the datastore."""
- if len(bug_ids) <= 1:
- logging.info('Deleting alias group due to too few bugs: %s', bug_ids)
- for vuln_id in bug_ids:
- changed_vulns[vuln_id] = None
- alias_group.key.delete()
- return
- if len(bug_ids) > ALIAS_GROUP_VULN_LIMIT:
- logging.info('Deleting alias group due to too many bugs: %s', bug_ids)
- for vuln_id in bug_ids:
- changed_vulns[vuln_id] = None
- alias_group.key.delete()
- return
-
- if bug_ids == alias_group.bug_ids:
- return
-
- alias_group.bug_ids = bug_ids
- alias_group.last_modified = datetime.datetime.now(datetime.UTC)
- alias_group.put()
- for vuln_id in bug_ids:
- changed_vulns[vuln_id] = alias_group
-
-
-def _create_alias_group(bug_ids: list[str],
- changed_vulns: dict[str, osv.AliasGroup | None]):
- """Creates a new alias group in the datastore."""
- if len(bug_ids) <= 1:
- logging.info('Skipping alias group creation due to too few bugs: %s',
- bug_ids)
- return
- if len(bug_ids) > ALIAS_GROUP_VULN_LIMIT:
- logging.info('Skipping alias group creation due to too many bugs: %s',
- bug_ids)
- return
-
- new_group = osv.AliasGroup(bug_ids=bug_ids)
- new_group.last_modified = datetime.datetime.now(datetime.UTC)
- new_group.put()
- for vuln_id in bug_ids:
- changed_vulns[vuln_id] = new_group
-
-
-def _compute_aliases(bug_id: str, visited: set[str],
- bug_aliases: dict[str, set[str]]) -> list[str]:
- """Computes all aliases for the given bug ID.
- The returned list contains the bug ID itself, all the IDs from the bug's
- raw aliases, all the IDs of bugs that have the current bug as an alias,
- and repeat for every bug encountered here."""
- to_visit = {bug_id}
- bug_ids = []
- while to_visit:
- bug_id = to_visit.pop()
- if bug_id in visited:
- continue
- visited.add(bug_id)
- bug_ids.append(bug_id)
-
- aliases = bug_aliases.get(bug_id, set())
- to_visit.update(aliases - visited)
-
- # Returns a sorted list of bug IDs, which ensures deterministic behaviour
- # and avoids unnecessary updates to the groups.
- return sorted(bug_ids)
-
-
-def _update_vuln_with_group(vuln_id: str, alias_group: osv.AliasGroup | None):
- """Updates the Vulnerability in Datastore & GCS with the new alias group.
- If `alias_group` is None, assumes a preexisting AliasGroup was just deleted.
- """
- # Get the existing vulnerability first, so we can recalculate search_indices
- result = gcs.get_by_id_with_generation(vuln_id)
- if result is None:
- if osv.Vulnerability.get_by_id(vuln_id) is not None:
- logging.error('vulnerability not in GCS - %s', vuln_id)
- pubsub.publish_failure(b'', type='gcs_missing', id=vuln_id)
- return
- vuln_proto, generation = result
-
- def transaction():
- vuln: osv.Vulnerability = osv.Vulnerability.get_by_id(vuln_id)
- if vuln is None:
- logging.error('vulnerability not in Datastore - %s', vuln_id)
- # TODO(michaelkedar): What to do in this case?
- return
- if alias_group is None:
- modified = datetime.datetime.now(datetime.UTC)
- aliases = []
- else:
- modified = alias_group.last_modified
- aliases = sorted(set(alias_group.bug_ids) - {vuln_id})
- vuln_proto.aliases[:] = aliases
- vuln_proto.modified.FromDatetime(modified)
- osv.ListedVulnerability.from_vulnerability(vuln_proto).put()
- vuln.modified = modified
- vuln.put()
-
- ndb.transaction(transaction)
- try:
- gcs.upload_vulnerability(vuln_proto, generation)
- except exceptions.PreconditionFailed:
- logging.error('Generation mismatch when writing aliases for %s', vuln_id)
- osv.pubsub.publish_failure(
- b'', type='gcs_gen_mismatch', id=vuln_id, field='aliases')
- except Exception:
- logging.error('Writing to bucket failed for %s', vuln_id)
- osv.pubsub.publish_failure(
- vuln_proto.SerializeToString(deterministic=True), type='gcs_retry')
-
-
-def run():
- """Updates all alias groups in the datastore by re-computing existing
- AliasGroups and creating new AliasGroups for un-computed bugs."""
-
- # Query for all bugs that have aliases.
- # Use (> '' OR < '') instead of (!= '') / (> '') to de-duplicate results
- # and avoid datastore emulator problems, see issue #2093
- bugs = osv.Bug.query(ndb.OR(osv.Bug.aliases > '', osv.Bug.aliases < ''))
- all_alias_group = osv.AliasGroup.query()
- allow_list = {
- allow_entry.bug_id for allow_entry in osv.AliasAllowListEntry.query()
- }
- deny_list = {
- deny_entry.bug_id for deny_entry in osv.AliasDenyListEntry.query()
- }
-
- # Mapping of ID to a set of all aliases for that bug,
- # including its raw aliases and bugs that it is referenced in as an alias.
- bug_aliases = {}
-
- # For each bug, add its aliases to the maps and ignore invalid bugs.
- for bug in bugs:
- if bug.db_id in deny_list:
- continue
- if len(bug.aliases) > VULN_ALIASES_LIMIT and bug.db_id not in allow_list:
- logging.info('%s has too many listed aliases, skipping computation.',
- bug.db_id)
- continue
- if bug.status != osv.BugStatus.PROCESSED:
- continue
- for alias in bug.aliases:
- bug_aliases.setdefault(bug.db_id, set()).add(alias)
- bug_aliases.setdefault(alias, set()).add(bug.db_id)
-
- visited = set()
-
- # Keep track of vulnerabilities that have been modified, to update GCS later.
- # `None` means the AliasGroup has been removed.
- changed_vulns: dict[str, osv.AliasGroup | None] = {}
-
- # For each alias group, re-compute the bug IDs in the group and update the
- # group with the computed bug IDs.
- for alias_group in all_alias_group:
- bug_id = alias_group.bug_ids[0] # AliasGroups contain more than one bug.
- # If the bug has already been counted in a different alias group,
- # we delete the original one to merge two alias groups.
- if bug_id in visited:
- for vuln_id in alias_group.bug_ids:
- if vuln_id not in changed_vulns:
- changed_vulns[vuln_id] = None
- alias_group.key.delete()
- continue
- bug_ids = _compute_aliases(bug_id, visited, bug_aliases)
- _update_group(bug_ids, alias_group, changed_vulns)
-
- # For each bug ID that has not been visited, create new alias groups.
- for bug_id in bug_aliases:
- if bug_id not in visited:
- bug_ids = _compute_aliases(bug_id, visited, bug_aliases)
- _create_alias_group(bug_ids, changed_vulns)
-
- # For each updated vulnerability, update them in Datastore & GCS
- for vuln_id, alias_group in changed_vulns.items():
- _update_vuln_with_group(vuln_id, alias_group)
diff --git a/gcp/website/emulator_upstream.py b/gcp/website/emulator_upstream.py
deleted file mode 100644
index 9a06771aaf3..00000000000
--- a/gcp/website/emulator_upstream.py
+++ /dev/null
@@ -1,243 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2025 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""OSV Upstream relation computation."""
-
-from collections import defaultdict
-import datetime
-import json
-import logging
-
-from google.cloud import exceptions
-from google.cloud import ndb
-
-import osv
-from osv import gcs, pubsub
-
-
-def compute_upstream(target_bug, bugs: dict[str, set[str]]) -> list[str]:
- """Computes all upstream vulnerabilities for the given bug ID.
- The returned list contains all of the bug IDs that are upstream of the
- target bug ID, including transitive upstreams."""
- visited = set()
-
- target_bug_upstream = target_bug
- if not target_bug_upstream:
- return []
- to_visit = set(target_bug_upstream)
- while to_visit:
- bug_id = to_visit.pop()
- if bug_id in visited:
- continue
- visited.add(bug_id)
- upstreams = set()
- if bug_id in bugs.keys():
- bug = bugs.get(bug_id)
- upstreams = set(bug)
-
- to_visit.update(upstreams - visited)
-
- # Returns a sorted list of bug IDs, which ensures deterministic behaviour
- # and avoids unnecessary updates.
- return sorted(visited)
-
-
-def _create_group(bug_id: str, upstream_ids: list[str]) -> osv.UpstreamGroup:
- """Creates a new upstream group in the datastore."""
-
- new_group = osv.UpstreamGroup(
- id=bug_id,
- db_id=bug_id,
- upstream_ids=upstream_ids,
- last_modified=datetime.datetime.now(datetime.UTC))
- new_group.put()
- _update_vuln_with_group(bug_id, new_group)
-
- return new_group
-
-
-def _update_group(upstream_group: osv.UpstreamGroup,
- upstream_ids: list[str]) -> osv.UpstreamGroup | None:
- """Updates the upstream group in the datastore."""
- if len(upstream_ids) == 0:
- logging.info('Deleting upstream group due to too few bugs: %s',
- upstream_ids)
- upstream_group.key.delete()
- _update_vuln_with_group(upstream_group.db_id, None)
- return None
-
- if upstream_ids == upstream_group.upstream_ids:
- return None
-
- upstream_group.upstream_ids = upstream_ids
- upstream_group.last_modified = datetime.datetime.now(datetime.UTC)
- upstream_group.put()
- _update_vuln_with_group(upstream_group.db_id, upstream_group)
- return upstream_group
-
-
-def _update_vuln_with_group(vuln_id: str, upstream: osv.UpstreamGroup | None):
- """Updates the Vulnerability in Datastore & GCS with the new upstream group.
- If `upstream` is None, assumes a preexisting UpstreamGroup was just deleted.
- """
- # Get the existing vulnerability first, so we can recalculate search_indices
- result = gcs.get_by_id_with_generation(vuln_id)
- if result is None:
- logging.error('vulnerability not in GCS - %s', vuln_id)
- pubsub.publish_failure(b'', type='gcs_missing', id=vuln_id)
- return
- vuln_proto, generation = result
-
- def transaction():
- vuln: osv.Vulnerability = osv.Vulnerability.get_by_id(vuln_id)
- if vuln is None:
- logging.error('vulnerability not in Datastore - %s', vuln_id)
- # TODO(michaelkedar): What to do in this case?
- return
- if upstream is None:
- modified = datetime.datetime.now(datetime.UTC)
- upstream_group = []
- else:
- modified = upstream.last_modified
- upstream_group = upstream.upstream_ids
- vuln_proto.upstream[:] = upstream_group
- vuln_proto.modified.FromDatetime(modified)
- osv.ListedVulnerability.from_vulnerability(vuln_proto).put()
- vuln.modified = modified
- vuln.put()
-
- ndb.transaction(transaction)
- try:
- gcs.upload_vulnerability(vuln_proto, generation)
- except exceptions.PreconditionFailed:
- logging.error('Generation mismatch when writing upstream for %s', vuln_id)
- osv.pubsub.publish_failure(
- b'', type='gcs_gen_mismatch', id=vuln_id, field='upstream')
- except Exception:
- logging.error('Writing to bucket failed for %s', vuln_id)
- osv.pubsub.publish_failure(
- vuln_proto.SerializeToString(deterministic=True), type='gcs_retry')
-
-
-def compute_upstream_hierarchy(
- target_upstream_group: osv.UpstreamGroup,
- all_upstream_groups: dict[str, osv.UpstreamGroup]) -> None:
- """Computes all upstream vulnerabilities for the given bug ID.
- The returned list contains all of the bug IDs that are upstream of the
- target bug ID, including transitive upstreams in a map hierarchy.
- upstream_group:
- { db_id: bug id
- upstream_ids: list of upstream bug ids
- last_modified_date: date
- upstream_hierarchy: JSON string of upstream hierarchy
- }
- """
-
- # To convert to json, sets need to be converted to lists
- # and sorting is done for a more consistent outcome.
- def set_default(obj):
- if isinstance(obj, set):
- return list(sorted(obj))
- raise TypeError
-
- visited = set()
- upstream_map = {}
- to_visit = set([target_upstream_group.db_id])
- # BFS navigation through the upstream hierarchy of a given upstream group
- while to_visit:
- bug_id = to_visit.pop()
- if bug_id in visited:
- continue
- visited.add(bug_id)
- upstream_group = all_upstream_groups.get(bug_id)
- if upstream_group is None:
- continue
-
- upstreams = set(upstream_group.upstream_ids)
- if not upstreams:
- continue
- for upstream in upstreams:
- if upstream not in visited and upstream not in to_visit:
- to_visit.add(upstream)
- else:
- if bug_id not in upstream_map:
- upstream_map[bug_id] = set([upstream])
- else:
- upstream_map[bug_id].add(upstream)
- # Add the immediate upstreams of the bug to the dict
- upstream_map[bug_id] = upstreams
- to_visit.update(upstreams - visited)
-
- # Ensure there are no duplicate entries where transitive vulns appear
- for k, v in upstream_map.items():
- if k is target_upstream_group.db_id:
- continue
- upstream_map[target_upstream_group
- .db_id] = upstream_map[target_upstream_group.db_id] - v
-
- # Update the datastore entry if hierarchy has changed
- if upstream_map:
- upstream_json = json.dumps(upstream_map, default=set_default)
- if upstream_json == target_upstream_group.upstream_hierarchy:
- return
- target_upstream_group.upstream_hierarchy = upstream_json
- target_upstream_group.put()
-
-
-def run():
- """Updates all upstream groups in the datastore by re-computing existing
- UpstreamGroups and creating new UpstreamGroups for un-computed bugs."""
-
- # Query for all bugs that have upstreams.
- updated_bugs = []
- logging.info('Retrieving bugs...')
- bugs_query = osv.Bug.query(osv.Bug.upstream_raw > '')
-
- bugs = defaultdict(set)
- for bug in bugs_query.iter(projection=[osv.Bug.db_id, osv.Bug.upstream_raw]):
- bugs[bug.db_id].add(bug.upstream_raw[0])
- logging.info('%s Bugs successfully retrieved', len(bugs))
-
- logging.info('Retrieving upstream groups...')
- upstream_groups = {
- group.db_id: group for group in osv.UpstreamGroup.query().iter()
- }
- logging.info('Upstream Groups successfully retrieved')
-
- for bug_id, bug in bugs.items():
- # Get the specific upstream_group ID
- upstream_group = upstream_groups.get(bug_id)
- # Recompute the transitive upstreams and compare with the existing group
- upstream_ids = compute_upstream(bug, bugs)
- if upstream_group:
- if upstream_ids == upstream_group.upstream_ids:
- continue
- # Update the existing UpstreamGroup
- new_upstream_group = _update_group(upstream_group, upstream_ids)
- if new_upstream_group is None:
- continue
- updated_bugs.append(new_upstream_group)
- upstream_groups[bug_id] = new_upstream_group
- logging.info('Upstream group updated for bug: %s', bug_id)
- else:
- # Create a new UpstreamGroup
- new_upstream_group = _create_group(bug_id, upstream_ids)
- logging.info('New upstream group created for bug: %s', bug_id)
- updated_bugs.append(new_upstream_group)
- upstream_groups[bug_id] = new_upstream_group
-
- for group in updated_bugs:
- # Recompute the upstream hierarchies
- compute_upstream_hierarchy(group, upstream_groups)
- logging.info('Upstream hierarchy updated for bug: %s', group.db_id)
diff --git a/gcp/website/frontend3/src/base.html b/gcp/website/frontend3/src/base.html
deleted file mode 100644
index 229c09a64c1..00000000000
--- a/gcp/website/frontend3/src/base.html
+++ /dev/null
@@ -1,125 +0,0 @@
-
-
-
-
- {% if has_importfindings %}
- The record has not been successfully imported from its source
- {% endif %}
- Please see our
- FAQ for more information.
-
- {% if ecosystem_counts %}
- {% set total = ecosystem_counts.values() | sum %}
- {% for ecosystem, count in ecosystem_counts.items() %}
- {% if count > 30 %}
-
- 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.
-
- 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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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.
- {% 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-") -%}
-