forked from mirrors/pkg-proxy
* Fix all golangci-lint issues across the codebase Resolve 77 lint issues reported by golangci-lint with gocritic, gocognit, gocyclo, maintidx, dupl, mnd, unparam, ireturn, goconst, and errcheck enabled. Net reduction of ~175 lines through shared helpers and deduplication. * Suppress staticcheck SA1019 for intentional deprecated field usage The Storage.Path field is deprecated but still read for backwards compatibility with existing configs that haven't migrated to the URL field.
408 lines
11 KiB
Go
408 lines
11 KiB
Go
package database
|
|
|
|
import "fmt"
|
|
|
|
const postgresTimestamp = "TIMESTAMP"
|
|
|
|
// Schema for proxy-specific tables. The packages and versions tables
|
|
// are compatible with git-pkgs, allowing the proxy to use an existing
|
|
// git-pkgs database as a starting point.
|
|
|
|
var schemaSQLite = `
|
|
CREATE TABLE IF NOT EXISTS schema_info (
|
|
version INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS packages (
|
|
id INTEGER PRIMARY KEY,
|
|
purl TEXT NOT NULL,
|
|
ecosystem TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
latest_version TEXT,
|
|
license TEXT,
|
|
description TEXT,
|
|
homepage TEXT,
|
|
repository_url TEXT,
|
|
registry_url TEXT,
|
|
supplier_name TEXT,
|
|
supplier_type TEXT,
|
|
source TEXT,
|
|
enriched_at DATETIME,
|
|
vulns_synced_at DATETIME,
|
|
created_at DATETIME,
|
|
updated_at DATETIME
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_packages_purl ON packages(purl);
|
|
CREATE INDEX IF NOT EXISTS idx_packages_ecosystem_name ON packages(ecosystem, name);
|
|
|
|
CREATE TABLE IF NOT EXISTS versions (
|
|
id INTEGER PRIMARY KEY,
|
|
purl TEXT NOT NULL,
|
|
package_purl TEXT NOT NULL,
|
|
license TEXT,
|
|
published_at DATETIME,
|
|
integrity TEXT,
|
|
yanked INTEGER DEFAULT 0,
|
|
source TEXT,
|
|
enriched_at DATETIME,
|
|
created_at DATETIME,
|
|
updated_at DATETIME
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_versions_purl ON versions(purl);
|
|
CREATE INDEX IF NOT EXISTS idx_versions_package_purl ON versions(package_purl);
|
|
|
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
id INTEGER PRIMARY KEY,
|
|
version_purl TEXT NOT NULL,
|
|
filename TEXT NOT NULL,
|
|
upstream_url TEXT NOT NULL,
|
|
storage_path TEXT,
|
|
content_hash TEXT,
|
|
size INTEGER,
|
|
content_type TEXT,
|
|
fetched_at DATETIME,
|
|
hit_count INTEGER DEFAULT 0,
|
|
last_accessed_at DATETIME,
|
|
created_at DATETIME,
|
|
updated_at DATETIME
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_artifacts_version_filename ON artifacts(version_purl, filename);
|
|
CREATE INDEX IF NOT EXISTS idx_artifacts_storage_path ON artifacts(storage_path);
|
|
CREATE INDEX IF NOT EXISTS idx_artifacts_last_accessed ON artifacts(last_accessed_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS vulnerabilities (
|
|
id INTEGER PRIMARY KEY,
|
|
vuln_id TEXT NOT NULL,
|
|
ecosystem TEXT NOT NULL,
|
|
package_name TEXT NOT NULL,
|
|
severity TEXT,
|
|
summary TEXT,
|
|
fixed_version TEXT,
|
|
cvss_score REAL,
|
|
"references" TEXT,
|
|
fetched_at DATETIME,
|
|
created_at DATETIME,
|
|
updated_at DATETIME
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_vulns_id_pkg ON vulnerabilities(vuln_id, ecosystem, package_name);
|
|
CREATE INDEX IF NOT EXISTS idx_vulns_ecosystem_pkg ON vulnerabilities(ecosystem, package_name);
|
|
`
|
|
|
|
var schemaPostgres = `
|
|
CREATE TABLE IF NOT EXISTS schema_info (
|
|
version INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS packages (
|
|
id SERIAL PRIMARY KEY,
|
|
purl TEXT NOT NULL,
|
|
ecosystem TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
latest_version TEXT,
|
|
license TEXT,
|
|
description TEXT,
|
|
homepage TEXT,
|
|
repository_url TEXT,
|
|
registry_url TEXT,
|
|
supplier_name TEXT,
|
|
supplier_type TEXT,
|
|
source TEXT,
|
|
enriched_at TIMESTAMP,
|
|
vulns_synced_at TIMESTAMP,
|
|
created_at TIMESTAMP,
|
|
updated_at TIMESTAMP
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_packages_purl ON packages(purl);
|
|
CREATE INDEX IF NOT EXISTS idx_packages_ecosystem_name ON packages(ecosystem, name);
|
|
|
|
CREATE TABLE IF NOT EXISTS versions (
|
|
id SERIAL PRIMARY KEY,
|
|
purl TEXT NOT NULL,
|
|
package_purl TEXT NOT NULL,
|
|
license TEXT,
|
|
published_at TIMESTAMP,
|
|
integrity TEXT,
|
|
yanked BOOLEAN DEFAULT FALSE,
|
|
source TEXT,
|
|
enriched_at TIMESTAMP,
|
|
created_at TIMESTAMP,
|
|
updated_at TIMESTAMP
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_versions_purl ON versions(purl);
|
|
CREATE INDEX IF NOT EXISTS idx_versions_package_purl ON versions(package_purl);
|
|
|
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
id SERIAL PRIMARY KEY,
|
|
version_purl TEXT NOT NULL,
|
|
filename TEXT NOT NULL,
|
|
upstream_url TEXT NOT NULL,
|
|
storage_path TEXT,
|
|
content_hash TEXT,
|
|
size BIGINT,
|
|
content_type TEXT,
|
|
fetched_at TIMESTAMP,
|
|
hit_count BIGINT DEFAULT 0,
|
|
last_accessed_at TIMESTAMP,
|
|
created_at TIMESTAMP,
|
|
updated_at TIMESTAMP
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_artifacts_version_filename ON artifacts(version_purl, filename);
|
|
CREATE INDEX IF NOT EXISTS idx_artifacts_storage_path ON artifacts(storage_path);
|
|
CREATE INDEX IF NOT EXISTS idx_artifacts_last_accessed ON artifacts(last_accessed_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS vulnerabilities (
|
|
id SERIAL PRIMARY KEY,
|
|
vuln_id TEXT NOT NULL,
|
|
ecosystem TEXT NOT NULL,
|
|
package_name TEXT NOT NULL,
|
|
severity TEXT,
|
|
summary TEXT,
|
|
fixed_version TEXT,
|
|
cvss_score REAL,
|
|
"references" TEXT,
|
|
fetched_at TIMESTAMP,
|
|
created_at TIMESTAMP,
|
|
updated_at TIMESTAMP
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_vulns_id_pkg ON vulnerabilities(vuln_id, ecosystem, package_name);
|
|
CREATE INDEX IF NOT EXISTS idx_vulns_ecosystem_pkg ON vulnerabilities(ecosystem, package_name);
|
|
`
|
|
|
|
// schemaArtifactsOnly contains just the artifacts table for adding to existing git-pkgs databases.
|
|
var schemaArtifactsSQLite = `
|
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
id INTEGER PRIMARY KEY,
|
|
version_purl TEXT NOT NULL,
|
|
filename TEXT NOT NULL,
|
|
upstream_url TEXT NOT NULL,
|
|
storage_path TEXT,
|
|
content_hash TEXT,
|
|
size INTEGER,
|
|
content_type TEXT,
|
|
fetched_at DATETIME,
|
|
hit_count INTEGER DEFAULT 0,
|
|
last_accessed_at DATETIME,
|
|
created_at DATETIME,
|
|
updated_at DATETIME
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_artifacts_version_filename ON artifacts(version_purl, filename);
|
|
CREATE INDEX IF NOT EXISTS idx_artifacts_storage_path ON artifacts(storage_path);
|
|
CREATE INDEX IF NOT EXISTS idx_artifacts_last_accessed ON artifacts(last_accessed_at);
|
|
`
|
|
|
|
var schemaArtifactsPostgres = `
|
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
id SERIAL PRIMARY KEY,
|
|
version_purl TEXT NOT NULL,
|
|
filename TEXT NOT NULL,
|
|
upstream_url TEXT NOT NULL,
|
|
storage_path TEXT,
|
|
content_hash TEXT,
|
|
size BIGINT,
|
|
content_type TEXT,
|
|
fetched_at TIMESTAMP,
|
|
hit_count BIGINT DEFAULT 0,
|
|
last_accessed_at TIMESTAMP,
|
|
created_at TIMESTAMP,
|
|
updated_at TIMESTAMP
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_artifacts_version_filename ON artifacts(version_purl, filename);
|
|
CREATE INDEX IF NOT EXISTS idx_artifacts_storage_path ON artifacts(storage_path);
|
|
CREATE INDEX IF NOT EXISTS idx_artifacts_last_accessed ON artifacts(last_accessed_at);
|
|
`
|
|
|
|
func (db *DB) CreateSchema() error {
|
|
if err := db.OptimizeForBulkWrites(); err != nil {
|
|
return err
|
|
}
|
|
|
|
var schema string
|
|
if db.dialect == DialectPostgres {
|
|
schema = schemaPostgres
|
|
} else {
|
|
schema = schemaSQLite
|
|
}
|
|
|
|
if _, err := db.Exec(schema); err != nil {
|
|
return fmt.Errorf("executing schema: %w", err)
|
|
}
|
|
|
|
query := db.Rebind("INSERT INTO schema_info (version) VALUES (?)")
|
|
if _, err := db.Exec(query, SchemaVersion); err != nil {
|
|
return fmt.Errorf("setting schema version: %w", err)
|
|
}
|
|
|
|
return db.OptimizeForReads()
|
|
}
|
|
|
|
// EnsureArtifactsTable adds the artifacts table to an existing database
|
|
// (e.g., a git-pkgs database) if it doesn't already exist.
|
|
func (db *DB) EnsureArtifactsTable() error {
|
|
var schema string
|
|
if db.dialect == DialectPostgres {
|
|
schema = schemaArtifactsPostgres
|
|
} else {
|
|
schema = schemaArtifactsSQLite
|
|
}
|
|
|
|
if _, err := db.Exec(schema); err != nil {
|
|
return fmt.Errorf("creating artifacts table: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (db *DB) SchemaVersion() (int, error) {
|
|
var version int
|
|
err := db.Get(&version, "SELECT version FROM schema_info LIMIT 1")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return version, nil
|
|
}
|
|
|
|
// HasTable checks if a table exists in the database.
|
|
func (db *DB) HasTable(name string) (bool, error) {
|
|
var exists bool
|
|
var query string
|
|
|
|
if db.dialect == DialectPostgres {
|
|
query = "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)"
|
|
} else {
|
|
query = "SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE type='table' AND name=?)"
|
|
}
|
|
|
|
err := db.Get(&exists, query, name)
|
|
return exists, err
|
|
}
|
|
|
|
// HasColumn checks if a column exists in a table.
|
|
func (db *DB) HasColumn(table, column string) (bool, error) {
|
|
var exists bool
|
|
var query string
|
|
|
|
if db.dialect == DialectPostgres {
|
|
query = "SELECT EXISTS (SELECT FROM information_schema.columns WHERE table_name = $1 AND column_name = $2)"
|
|
} else {
|
|
// For SQLite, check table_info
|
|
query = "SELECT COUNT(*) > 0 FROM pragma_table_info(?) WHERE name = ?"
|
|
}
|
|
|
|
err := db.Get(&exists, query, table, column)
|
|
return exists, err
|
|
}
|
|
|
|
// MigrateSchema adds missing columns to existing tables for backward compatibility.
|
|
func (db *DB) MigrateSchema() error {
|
|
// Check and add missing columns to packages table
|
|
packagesColumns := map[string]string{
|
|
"registry_url": "TEXT",
|
|
"supplier_name": "TEXT",
|
|
"supplier_type": "TEXT",
|
|
"source": "TEXT",
|
|
"enriched_at": "DATETIME",
|
|
"vulns_synced_at": "DATETIME",
|
|
}
|
|
|
|
if db.dialect == DialectPostgres {
|
|
packagesColumns["enriched_at"] = postgresTimestamp
|
|
packagesColumns["vulns_synced_at"] = postgresTimestamp
|
|
}
|
|
|
|
for column, colType := range packagesColumns {
|
|
hasCol, err := db.HasColumn("packages", column)
|
|
if err != nil {
|
|
return fmt.Errorf("checking column %s: %w", column, err)
|
|
}
|
|
if !hasCol {
|
|
alterQuery := fmt.Sprintf("ALTER TABLE packages ADD COLUMN %s %s", column, colType)
|
|
if _, err := db.Exec(alterQuery); err != nil {
|
|
return fmt.Errorf("adding column %s to packages: %w", column, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check and add missing columns to versions table
|
|
versionsColumns := map[string]string{
|
|
"integrity": "TEXT",
|
|
"yanked": "INTEGER DEFAULT 0",
|
|
"source": "TEXT",
|
|
"enriched_at": "DATETIME",
|
|
}
|
|
|
|
if db.dialect == DialectPostgres {
|
|
versionsColumns["yanked"] = "BOOLEAN DEFAULT FALSE"
|
|
versionsColumns["enriched_at"] = postgresTimestamp
|
|
}
|
|
|
|
for column, colType := range versionsColumns {
|
|
hasCol, err := db.HasColumn("versions", column)
|
|
if err != nil {
|
|
return fmt.Errorf("checking column %s: %w", column, err)
|
|
}
|
|
if !hasCol {
|
|
alterQuery := fmt.Sprintf("ALTER TABLE versions ADD COLUMN %s %s", column, colType)
|
|
if _, err := db.Exec(alterQuery); err != nil {
|
|
return fmt.Errorf("adding column %s to versions: %w", column, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure artifacts table exists
|
|
if err := db.EnsureArtifactsTable(); err != nil {
|
|
return fmt.Errorf("ensuring artifacts table: %w", err)
|
|
}
|
|
|
|
// Ensure vulnerabilities table exists
|
|
hasVulns, err := db.HasTable("vulnerabilities")
|
|
if err != nil {
|
|
return fmt.Errorf("checking vulnerabilities table: %w", err)
|
|
}
|
|
if !hasVulns {
|
|
var vulnSchema string
|
|
if db.dialect == DialectPostgres {
|
|
vulnSchema = `
|
|
CREATE TABLE vulnerabilities (
|
|
id SERIAL PRIMARY KEY,
|
|
vuln_id TEXT NOT NULL,
|
|
ecosystem TEXT NOT NULL,
|
|
package_name TEXT NOT NULL,
|
|
severity TEXT,
|
|
summary TEXT,
|
|
fixed_version TEXT,
|
|
cvss_score REAL,
|
|
"references" TEXT,
|
|
fetched_at TIMESTAMP,
|
|
created_at TIMESTAMP,
|
|
updated_at TIMESTAMP
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_vulns_id_pkg ON vulnerabilities(vuln_id, ecosystem, package_name);
|
|
CREATE INDEX IF NOT EXISTS idx_vulns_ecosystem_pkg ON vulnerabilities(ecosystem, package_name);
|
|
`
|
|
} else {
|
|
vulnSchema = `
|
|
CREATE TABLE vulnerabilities (
|
|
id INTEGER PRIMARY KEY,
|
|
vuln_id TEXT NOT NULL,
|
|
ecosystem TEXT NOT NULL,
|
|
package_name TEXT NOT NULL,
|
|
severity TEXT,
|
|
summary TEXT,
|
|
fixed_version TEXT,
|
|
cvss_score REAL,
|
|
"references" TEXT,
|
|
fetched_at DATETIME,
|
|
created_at DATETIME,
|
|
updated_at DATETIME
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_vulns_id_pkg ON vulnerabilities(vuln_id, ecosystem, package_name);
|
|
CREATE INDEX IF NOT EXISTS idx_vulns_ecosystem_pkg ON vulnerabilities(ecosystem, package_name);
|
|
`
|
|
}
|
|
if _, err := db.Exec(vulnSchema); err != nil {
|
|
return fmt.Errorf("creating vulnerabilities table: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|