Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,15 @@ Many tests use expected outputs saved directly in the source tree:
```
Always inspect the resulting `git diff` to ensure the API query output changes are expected.

### Local UI & Datastore Emulator
- For local UI testing without GCP project credentials, run the website using a local mock dataset and a datastore emulator:
### Local UI & Website Development
- **Website DevServer (Go-native)**:
Run the local Go website development server against a live flat mock dataset with hot reloading (no GCP credentials or Datastore emulator required):
```bash
make run-website-devserver
```
- Mock vulnerability records are located in [`go/cmd/website-devserver/testdata/`](go/cmd/website-devserver/testdata/). Add or edit `.json` records and `.meta.yaml` companion files to immediately see changes on page refresh.
- **Python Website with Datastore Emulator (Legacy)**:
Run the legacy Python website server against a local Datastore emulator:
```bash
make run-website-emulator
```
Expand Down Expand Up @@ -193,35 +200,44 @@ All Go microservices are compiled using a single, unified multi-target Dockerfil
- Development orchestrator command for local testing.
- Spawns the Go API server natively in a background thread while concurrently running the `osv-esp` (ESPv2) docker container to perform HTTP/JSON to gRPC transcoding.

3. **`importer`**:
3. **`website`**:
- The public OSV website server implemented in Go.
- Defined in `go/cmd/website` and deployed to Cloud Run.

4. **`website-devserver`**:
- Local development server for the Go website frontend.
- Serves the website using a live flat mock dataset in `go/cmd/website-devserver/testdata/` with hot reloading (reads live from disk on every request without requiring GCP credentials or emulator).

5. **`importer`**:
- Run as a cron job.
- Reads from each vulnerability data source (defined as `SourceRepository` in Datastore or mapped in [`source.yaml`](source.yaml) / [`source_test.yaml`](source_test.yaml)).
- Detects new or deleted vulnerability records.
- Dispatches processing tasks via **GCP Pub/Sub** to the worker.

4. **`worker`**:
6. **`worker`**:
- Daemon that subscribes to Pub/Sub tasks.
- Ingests and enriches vulnerability records.
- Computes affected Git ranges for commit-based querying.
- Writes the enriched records to the database (GCS/Datastore).
- Powered by a modular processing pipeline defined in [`go/internal/worker/pipeline/`](go/internal/worker/pipeline/).

5. **`exporter`**:
7. **`exporter`**:
- Exports the entire database to a public GCS bucket.
- Generates a root `all.zip` file containing all records.
- Generates ecosystem-specific `all.zip` files (e.g., `PyPI/all.zip`).
- Outputs individual vulnerability JSON files in their respective ecosystem folders (e.g., `PyPI/GHSA-abcd-efgh.json`).

6. **`relations`**:
8. **`relations`**:
- Populates relationships between vulnerabilities in the database.
- Calculates transitive and reflective `aliases`, reflective `related` vulnerabilities, and transitive `upstream` fields.

7. **`gitter`**:
9. **`gitter`**:
- Git client daemon/utility to precompute and cache git operations required by other services.
- Performs intensive Git tasks like computing commit graphs and generating patch IDs.

### Internal Shared Libraries (`go/internal/`)
- **`api/`**: Shared package containing the core gRPC public server implementation of the OSV API.
- **`website/`**: Shared package implementing HTTP handlers, templates, routing, and search logic for the Go website frontend.
- **`worker/`**: Core engine and subscriber logic for the Go worker.
- **`database/`**: Shared Datastore client and repository models (specifically [`go/internal/database/datastore/`](go/internal/database/datastore/)).
- *Design Pattern*: Models here **mirror** the Datastore models defined in the Python library ([`osv/models.py`](osv/models.py)).
Expand Down Expand Up @@ -252,9 +268,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. The legacy Python implementation remains in `gcp/api/` but is retired.

### 2. Website (`gcp/website/`)
- **Status**: **Active**.
- Contains frontend/website code. Uses Python backend, Hugo for blog rendering, and pnpm for modern JS dependencies.
### 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/`).
- **Deployment Target**: **Google Cloud Run** (managed via Cloud Deploy pipeline `osv-website`).

### 3. Workers (`gcp/workers/`)
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ run-go-website: build-website-frontend ## Run local Go website against prod Data
run-go-website-staging: build-website-frontend
cd go && GOOGLE_CLOUD_PROJECT=oss-vdb-test OSV_VULNERABILITIES_BUCKET=osv-test-vulnerabilities go run ./cmd/website -static-dir ../website/dist -docs-dir ../docs

run-go-website-emulator: build-website-frontend ## Run local Go website against emulator
cd go && DATASTORE_EMULATOR_HOST=localhost:5002 go run ./cmd/website -static-dir ../website/dist -docs-dir ../docs
run-website-devserver: build-website-frontend ## Run local Go website development server against local mock dataset
cd go && go run ./cmd/website-devserver -data-dir cmd/website-devserver/testdata -static-dir ../website/dist -docs-dir ../docs

stage-website-assets: build-website-frontend
mkdir -p go/cmd/website/dist go/cmd/website/docs
Expand Down
136 changes: 136 additions & 0 deletions go/cmd/website-devserver/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Package main implements the entry point for the OSV website development server.
package main

import (
"context"
"errors"
"flag"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"time"

"github.com/google/osv.dev/go/internal/website"
"github.com/google/osv.dev/go/logger"
)

func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

func run() error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

staticDirFlag := flag.String("static-dir", "../website/dist", "Path to static asset directory (dist)")
docsDirFlag := flag.String("docs-dir", "../docs", "Path to documentation directory")
dataDirFlag := flag.String("data-dir", "cmd/website-devserver/testdata", "Path to mock vulnerabilities data directory")
sourcesFileFlag := flag.String("sources-file", "../source.yaml", "Path to source.yaml definitions")
portFlag := flag.Int("port", 8000, "Port to listen on (overridden by PORT env var if set)")
apiURLFlag := flag.String("api-url", "api.osv.dev", "API URL to use for links")
flag.Parse()

port := *portFlag
if portEnv := os.Getenv("PORT"); portEnv != "" {
if p, err := strconv.Atoi(portEnv); err == nil {
port = p
}
}

staticDir := *staticDirFlag
if _, err := os.Stat(staticDir); err != nil {
if _, err := os.Stat("dist"); err == nil {
staticDir = "dist"
}
}

dataDir := *dataDirFlag
if _, err := os.Stat(dataDir); err != nil {
// Fallback to testdata or ../gcp/website/testdata/osv
if _, err := os.Stat("testdata"); err == nil {
dataDir = "testdata"
} else if _, err := os.Stat("../gcp/website/testdata/osv"); err == nil {
dataDir = "../gcp/website/testdata/osv"
}
}

sourcesFile := *sourcesFileFlag
if _, err := os.Stat(sourcesFile); err != nil {
if _, err := os.Stat(filepath.Join(dataDir, "sources.yaml")); err == nil {
sourcesFile = filepath.Join(dataDir, "sources.yaml")
} else if _, err := os.Stat("testdata/sources.yaml"); err == nil {
sourcesFile = "testdata/sources.yaml"
} else {
sourcesFile = ""
}
}

staticFS := os.DirFS(staticDir)
docsFS := os.DirFS(*docsDirFlag)

devStore, err := NewDevStore(dataDir, sourcesFile)
if err != nil {
return fmt.Errorf("failed to initialize dev store: %w", err)
}

stores := website.Stores{
Vuln: devStore,
Relations: devStore,
SourceRepo: devStore,
VulnSearch: devStore,
Linter: devStore,
Triage: devStore,
}

srv, err := website.NewServer(website.Config{
StaticFS: staticFS,
DocsFS: docsFS,
Stores: stores,
APIURL: *apiURLFlag,
Auth: website.AuthConfig{
BypassOAuth: true,
},
})
if err != nil {
return fmt.Errorf("failed to create website server: %w", err)
}

httpServer := &http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: srv,
ReadHeaderTimeout: 10 * time.Second,
}

serverErr := make(chan error, 1)
go func() {
url := fmt.Sprintf("http://localhost:%d", port)
logger.InfoContext(ctx, "Starting website development server at "+url,
slog.String("url", url),
slog.Int("port", port),
slog.String("data_dir", dataDir),
slog.String("static_dir", staticDir),
)
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
serverErr <- err
}
}()

select {
case <-ctx.Done():
logger.InfoContext(ctx, "Shutting down website development server gracefully...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

return httpServer.Shutdown(shutdownCtx)
case err := <-serverErr:
return fmt.Errorf("server error: %w", err)
}
}
Loading