From a5c31b25f4aabbb7ee0b76c59ad1bb8f8aa92221 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Dec 2025 03:55:17 +0000 Subject: [PATCH 1/4] feat: implement vdb subcommand for Vulnerability Database API Implement comprehensive VDB (Vulnerability Database) API integration with the following components: Core Implementation: - internal/vdb/client.go: VDB API client with AWS SigV4 authentication * JWT token management with automatic caching and refresh * Support for environment variables and config file credentials * SHA-512 HMAC request signing for auth endpoint * HTTP client with proper timeout and error handling - internal/vdb/api.go: API endpoint methods * GetCVE: Retrieve CVE information * GetEcosystems: List available package ecosystems * GetProductVersions: Get product versions with pagination * GetProductVersion: Get specific version details * GetPackageVulnerabilities: Get package vulnerabilities * GetOpenAPISpec: Retrieve API specification CLI Commands (cmd/vdb.go): - vulnetix vdb cve : Get CVE details - vulnetix vdb ecosystems: List ecosystems - vulnetix vdb product [version]: Get product info - vulnetix vdb vulns : Get vulnerabilities - vulnetix vdb spec: Get OpenAPI specification Features: - AWS SigV4 (SHA-512) authentication - JWT token caching (15-minute expiry) - Pagination support (--limit, --offset) - Multiple output formats (json, pretty) - Flexible credential loading (env vars, config file, flags) - Rate limit awareness (60/min, 1000/week) - Comprehensive error handling Documentation: - docs/VDB-COMMAND.md: Complete command reference - docs/VDB-QUICKSTART.md: Quick start guide - VDB-IMPLEMENTATION.md: Implementation overview Examples: - examples/vdb-config.json: Configuration template - examples/vdb-ci-example.sh: CI/CD integration script - examples/vdb-github-action.yml: GitHub Actions workflow Based on VDB API User Guide v1 specifications. --- VDB-IMPLEMENTATION.md | 406 +++++++++++++++++++++++++++++++ cmd/vdb.go | 324 +++++++++++++++++++++++++ docs/VDB-COMMAND.md | 424 +++++++++++++++++++++++++++++++++ docs/VDB-QUICKSTART.md | 296 +++++++++++++++++++++++ examples/vdb-ci-example.sh | 108 +++++++++ examples/vdb-config.json | 4 + examples/vdb-github-action.yml | 158 ++++++++++++ internal/vdb/api.go | 176 ++++++++++++++ internal/vdb/client.go | 288 ++++++++++++++++++++++ 9 files changed, 2184 insertions(+) create mode 100644 VDB-IMPLEMENTATION.md create mode 100644 cmd/vdb.go create mode 100644 docs/VDB-COMMAND.md create mode 100644 docs/VDB-QUICKSTART.md create mode 100644 examples/vdb-ci-example.sh create mode 100644 examples/vdb-config.json create mode 100644 examples/vdb-github-action.yml create mode 100644 internal/vdb/api.go create mode 100644 internal/vdb/client.go diff --git a/VDB-IMPLEMENTATION.md b/VDB-IMPLEMENTATION.md new file mode 100644 index 0000000..c872b7c --- /dev/null +++ b/VDB-IMPLEMENTATION.md @@ -0,0 +1,406 @@ +# VDB Subcommand Implementation + +This document provides an overview of the VDB subcommand implementation for the Vulnetix CLI. + +## Overview + +The `vdb` subcommand provides comprehensive access to the Vulnetix Vulnerability Database (VDB) API, enabling users to: + +- Query CVE information +- List available package ecosystems +- Retrieve product/package versions +- Find vulnerabilities for specific packages +- Access the OpenAPI specification + +## Architecture + +### Components + +``` +vulnetix cli/ +├── cmd/ +│ └── vdb.go # Main VDB command and subcommands +├── internal/ +│ └── vdb/ +│ ├── client.go # VDB API client with authentication +│ └── api.go # API endpoint methods +├── docs/ +│ ├── VDB-COMMAND.md # Full command reference +│ └── VDB-QUICKSTART.md # Quick start guide +└── examples/ + ├── vdb-config.json # Example configuration + ├── vdb-ci-example.sh # CI/CD integration example + └── vdb-github-action.yml # GitHub Actions workflow +``` + +### Authentication Flow + +1. **Credential Loading**: + - Check command-line flags (`--org-id`, `--secret`) + - Check environment variables (`VVD_ORG`, `VVD_SECRET`) + - Check config file (`~/.vulnetix/vdb.json`) + +2. **Token Management**: + - Request JWT token using AWS SigV4 (SHA-512) authentication + - Cache token with expiration time + - Automatically refresh when expired (15-minute lifetime) + +3. **API Requests**: + - Use cached JWT token in `Authorization: Bearer ` header + - Handle rate limiting (60 req/min, 1000 req/week) + - Parse and display responses + +## Implementation Details + +### Client Package (`internal/vdb/client.go`) + +**Key Features**: +- AWS SigV4 SHA-512 request signing +- JWT token caching and automatic refresh +- Credential loading from multiple sources +- HTTP client with 30-second timeout +- Comprehensive error handling + +**Main Types**: +```go +type Client struct { + BaseURL string + OrgID string + SecretKey string + HTTPClient *http.Client + token *TokenCache +} + +type TokenCache struct { + Token string + ExpiresAt time.Time +} +``` + +**Key Methods**: +- `NewClient()` - Create new VDB client +- `GetToken()` - Get valid JWT token (from cache or new request) +- `DoRequest()` - Execute authenticated API request +- `signRequest()` - Sign request with AWS SigV4 +- `LoadCredentials()` - Load credentials from environment/config + +### API Package (`internal/vdb/api.go`) + +**Implemented Endpoints**: +- `GetCVE(cveID)` - Get CVE information +- `GetEcosystems()` - List ecosystems +- `GetProductVersions(name, limit, offset)` - List product versions +- `GetProductVersion(name, version)` - Get specific version info +- `GetPackageVulnerabilities(name, limit, offset)` - Get vulnerabilities +- `GetOpenAPISpec()` - Get API specification + +**Response Types**: +```go +type CVEInfo struct { + CVE string + Description string + Published string + Modified string + CVSS map[string]interface{} + References []interface{} + Data map[string]interface{} +} + +type ProductVersionsResponse struct { + PackageName string + Timestamp int64 + Total int + Limit int + Offset int + HasMore bool + Versions []string +} +``` + +### Command Structure (`cmd/vdb.go`) + +**Main Command**: +```bash +vulnetix vdb [subcommand] [flags] +``` + +**Subcommands**: +1. `cve ` - Get CVE information +2. `ecosystems` - List available ecosystems +3. `product [version]` - Get product/version info +4. `vulns ` - Get package vulnerabilities +5. `spec` - Get OpenAPI specification + +**Global Flags**: +- `--org-id` - Organization UUID +- `--secret` - Secret key +- `--base-url` - API base URL +- `-o, --output` - Output format (json, pretty) + +**Pagination Flags** (for `product` and `vulns`): +- `--limit` - Maximum results (default: 100) +- `--offset` - Results to skip (default: 0) + +## Security Considerations + +### Credential Management + +1. **Never hardcode credentials** in source code +2. **Use environment variables** for CI/CD +3. **Secure config files** with `chmod 600` +4. **Rotate credentials** regularly +5. **Use secrets managers** in production + +### Token Security + +- JWT tokens expire after 15 minutes +- Tokens cached in memory only (not persisted to disk) +- Automatic token refresh on expiration +- TLS/HTTPS for all API communication + +### Rate Limiting + +- Per-minute: 60 requests +- Per-week: 1000 requests (default, configurable) +- Rate limit headers in responses +- Graceful error handling + +## Usage Examples + +### Basic Commands + +```bash +# Get CVE information +vulnetix vdb cve CVE-2024-1234 + +# List ecosystems +vulnetix vdb ecosystems + +# Get product versions +vulnetix vdb product express + +# Get specific version +vulnetix vdb product express 4.17.1 + +# Get package vulnerabilities +vulnetix vdb vulns lodash +``` + +### With Output Formatting + +```bash +# JSON output +vulnetix vdb cve CVE-2024-1234 --output json + +# Pretty print (default) +vulnetix vdb ecosystems -o pretty + +# Save to file +vulnetix vdb spec -o json > api-spec.json +``` + +### With Pagination + +```bash +# Limit results +vulnetix vdb product react --limit 50 + +# Skip results +vulnetix vdb product react --offset 100 + +# Combine +vulnetix vdb vulns express --limit 20 --offset 40 +``` + +### CI/CD Integration + +```bash +# Set credentials in CI environment +export VVD_ORG="${SECRET_VVD_ORG}" +export VVD_SECRET="${SECRET_VVD_SECRET}" + +# Run vulnerability scan +vulnetix vdb vulns my-package -o json > report.json + +# Check for critical vulnerabilities +if vulnetix vdb vulns my-package -o json | jq '.vulnerabilities[] | select(.severity == "CRITICAL")' | grep -q .; then + echo "Critical vulnerabilities found!" + exit 1 +fi +``` + +## Testing + +### Manual Testing + +```bash +# 1. Set test credentials +export VVD_ORG="test-uuid" +export VVD_SECRET="test-secret" + +# 2. Test authentication +vulnetix vdb ecosystems + +# 3. Test each subcommand +vulnetix vdb cve CVE-2021-44228 +vulnetix vdb product express --limit 10 +vulnetix vdb vulns lodash +vulnetix vdb spec -o json +``` + +### Integration Testing + +See `examples/vdb-ci-example.sh` for a complete CI/CD test script. + +## Error Handling + +### Authentication Errors (401) + +- Missing or invalid credentials +- Expired JWT token +- Invalid signature + +**Solution**: Check credentials, token automatically refreshes + +### Rate Limiting (429) + +- Exceeded per-minute limit (60 req/min) +- Exceeded weekly quota (1000 req/week) + +**Solution**: Wait for reset time, implement backoff, request higher quota + +### Not Found (404) + +- CVE doesn't exist +- Package not found + +**Solution**: Verify identifier spelling + +### Server Errors (500) + +- Unexpected server error + +**Solution**: Retry with exponential backoff + +## Future Enhancements + +### Potential Features + +1. **Response Caching**: + - Local cache for frequently accessed data + - Configurable TTL + - Cache invalidation + +2. **Bulk Operations**: + - Batch CVE queries + - Bulk package scanning + - Parallel requests + +3. **Advanced Filtering**: + - Filter by severity + - Filter by date range + - CVSS score filtering + +4. **Report Generation**: + - HTML reports + - PDF exports + - Custom templates + +5. **Notification Integration**: + - Slack notifications + - Email alerts + - Webhook support + +6. **Database Export**: + - Export to SQLite + - PostgreSQL integration + - CSV exports + +## References + +### Documentation + +- [VDB API User Guide](./docs/VDB%20API%20User%20Guide%20v1.pdf) +- [Command Reference](./docs/VDB-COMMAND.md) +- [Quick Start Guide](./docs/VDB-QUICKSTART.md) + +### API Resources + +- **Base URL**: https://api.vdb.vulnetix.com/v1 +- **OpenAPI Spec**: https://api.vdb.vulnetix.com/v1/spec +- **Interactive Docs**: https://redocly.github.io/redoc/?url=https://api.vdb.vulnetix.com/v1/spec + +### Support + +- **Email**: sales@vulnetix.com +- **Website**: https://www.vulnetix.com +- **GitHub**: https://github.com/vulnetix/cli + +## Development + +### Building + +```bash +# Build for development +make dev + +# Build for all platforms +make build-all +``` + +### Adding New Endpoints + +1. Add method to `internal/vdb/api.go` +2. Add subcommand to `cmd/vdb.go` +3. Update documentation +4. Add examples + +Example: +```go +// internal/vdb/api.go +func (c *Client) GetNewEndpoint() (*Response, error) { + path := "/new-endpoint" + respBody, err := c.DoRequest("GET", path, nil) + if err != nil { + return nil, err + } + var resp Response + if err := json.Unmarshal(respBody, &resp); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + return &resp, nil +} + +// cmd/vdb.go +var newCmd = &cobra.Command{ + Use: "new", + Short: "Description", + RunE: func(cmd *cobra.Command, args []string) error { + client := vdb.NewClient(vdbOrgID, vdbSecretKey) + result, err := client.GetNewEndpoint() + if err != nil { + return err + } + return printOutput(result, vdbOutput) + }, +} + +func init() { + vdbCmd.AddCommand(newCmd) +} +``` + +## License + +This implementation follows the Vulnetix CLI license. VDB API data is licensed under MIT License. + +## Contributors + +- Vulnetix Engineering Team +- Claude Code (AI Assistant) + +--- + +**Status**: ✅ Implementation Complete + +**Last Updated**: 2025-12-30 diff --git a/cmd/vdb.go b/cmd/vdb.go new file mode 100644 index 0000000..52fab58 --- /dev/null +++ b/cmd/vdb.go @@ -0,0 +1,324 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/vulnetix/vulnetix/internal/vdb" +) + +var ( + vdbOrgID string + vdbSecretKey string + vdbBaseURL string + vdbLimit int + vdbOffset int + vdbOutput string +) + +// vdbCmd represents the vdb command +var vdbCmd = &cobra.Command{ + Use: "vdb", + Short: "Interact with the Vulnetix Vulnerability Database (VDB) API", + Long: `Access and query the Vulnetix Vulnerability Database (VDB) API. + +The VDB API provides comprehensive vulnerability intelligence from multiple authoritative sources +including MITRE CVE, NIST NVD, CISA KEV, and many others. + +Authentication: + Set credentials via environment variables: + export VVD_ORG="your-organization-uuid" + export VVD_SECRET="your-secret-key" + + Or create a config file at ~/.vulnetix/vdb.json: + { + "org_id": "your-organization-uuid", + "secret_key": "your-secret-key" + } + +Examples: + # Get information about a CVE + vulnetix vdb cve CVE-2024-1234 + + # List available ecosystems + vulnetix vdb ecosystems + + # Get versions for a product + vulnetix vdb product express + + # Get vulnerabilities for a package + vulnetix vdb vulns express`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Load credentials if not provided via flags + if vdbOrgID == "" || vdbSecretKey == "" { + orgID, secret, err := vdb.LoadCredentials() + if err != nil { + return err + } + if vdbOrgID == "" { + vdbOrgID = orgID + } + if vdbSecretKey == "" { + vdbSecretKey = secret + } + } + return nil + }, +} + +// cveCmd retrieves information about a specific CVE +var cveCmd = &cobra.Command{ + Use: "cve ", + Short: "Get information about a specific CVE", + Long: `Retrieve detailed vulnerability information for a specific CVE identifier. + +Examples: + vulnetix vdb cve CVE-2024-1234 + vulnetix vdb cve CVE-2024-1234 --output json + vulnetix vdb cve CVE-2024-1234 -o pretty`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cveID := args[0] + + client := vdb.NewClient(vdbOrgID, vdbSecretKey) + if vdbBaseURL != "" { + client.BaseURL = vdbBaseURL + } + + fmt.Printf("🔍 Fetching information for %s...\n", cveID) + + cveInfo, err := client.GetCVE(cveID) + if err != nil { + return fmt.Errorf("failed to get CVE: %w", err) + } + + return printOutput(cveInfo.Data, vdbOutput) + }, +} + +// ecosystemsCmd lists available ecosystems +var ecosystemsCmd = &cobra.Command{ + Use: "ecosystems", + Short: "List available package ecosystems", + Long: `List all available package ecosystems in the VDB. + +Examples: + vulnetix vdb ecosystems + vulnetix vdb ecosystems --output json`, + RunE: func(cmd *cobra.Command, args []string) error { + client := vdb.NewClient(vdbOrgID, vdbSecretKey) + if vdbBaseURL != "" { + client.BaseURL = vdbBaseURL + } + + fmt.Println("🌐 Fetching available ecosystems...") + + ecosystems, err := client.GetEcosystems() + if err != nil { + return fmt.Errorf("failed to get ecosystems: %w", err) + } + + if vdbOutput == "json" { + return printOutput(map[string]interface{}{"ecosystems": ecosystems}, vdbOutput) + } + + fmt.Printf("\n✅ Found %d ecosystems:\n\n", len(ecosystems)) + for _, eco := range ecosystems { + fmt.Printf(" • %s\n", eco) + } + + return nil + }, +} + +// productCmd retrieves product version information +var productCmd = &cobra.Command{ + Use: "product [version]", + Short: "Get product version information", + Long: `Retrieve version information for a specific product. + +If no version is specified, lists all available versions. +If a version is specified, retrieves detailed information for that version. + +Examples: + # List all versions + vulnetix vdb product express + + # Get specific version + vulnetix vdb product express 4.17.1 + + # With pagination + vulnetix vdb product express --limit 50 --offset 100`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + productName := args[0] + + client := vdb.NewClient(vdbOrgID, vdbSecretKey) + if vdbBaseURL != "" { + client.BaseURL = vdbBaseURL + } + + // If version is provided, get specific version info + if len(args) > 1 { + version := args[1] + fmt.Printf("🔍 Fetching information for %s@%s...\n", productName, version) + + info, err := client.GetProductVersion(productName, version) + if err != nil { + return fmt.Errorf("failed to get product version: %w", err) + } + + return printOutput(info, vdbOutput) + } + + // Otherwise, list all versions + fmt.Printf("📦 Fetching versions for %s...\n", productName) + + resp, err := client.GetProductVersions(productName, vdbLimit, vdbOffset) + if err != nil { + return fmt.Errorf("failed to get product versions: %w", err) + } + + if vdbOutput == "json" { + return printOutput(resp, vdbOutput) + } + + fmt.Printf("\n✅ Found %d total versions (showing %d):\n\n", resp.Total, len(resp.Versions)) + for i, version := range resp.Versions { + fmt.Printf(" %d. %s\n", i+1, version) + } + + if resp.HasMore { + fmt.Printf("\n💡 More results available. Use --offset %d to see more.\n", resp.Offset+resp.Limit) + } + + return nil + }, +} + +// vulnsCmd retrieves vulnerabilities for a package +var vulnsCmd = &cobra.Command{ + Use: "vulns ", + Short: "Get vulnerabilities for a package", + Long: `Retrieve all known vulnerabilities for a specific package. + +Examples: + vulnetix vdb vulns express + vulnetix vdb vulns express --limit 50 + vulnetix vdb vulns express --output json`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + packageName := args[0] + + client := vdb.NewClient(vdbOrgID, vdbSecretKey) + if vdbBaseURL != "" { + client.BaseURL = vdbBaseURL + } + + fmt.Printf("🔒 Fetching vulnerabilities for %s...\n", packageName) + + resp, err := client.GetPackageVulnerabilities(packageName, vdbLimit, vdbOffset) + if err != nil { + return fmt.Errorf("failed to get vulnerabilities: %w", err) + } + + if vdbOutput == "json" { + return printOutput(resp, vdbOutput) + } + + fmt.Printf("\n⚠️ Found %d total vulnerabilities (showing %d):\n\n", resp.Total, len(resp.Vulnerabilities)) + for i, vuln := range resp.Vulnerabilities { + cveID := "Unknown" + if id, ok := vuln["cve"].(string); ok { + cveID = id + } else if id, ok := vuln["id"].(string); ok { + cveID = id + } + + fmt.Printf(" %d. %s\n", i+1, cveID) + + // Print severity if available + if severity, ok := vuln["severity"].(string); ok { + fmt.Printf(" Severity: %s\n", severity) + } + } + + if resp.HasMore { + fmt.Printf("\n💡 More results available. Use --offset %d to see more.\n", resp.Offset+resp.Limit) + } + + return nil + }, +} + +// specCmd retrieves the OpenAPI specification +var specCmd = &cobra.Command{ + Use: "spec", + Short: "Get the OpenAPI specification", + Long: `Retrieve the OpenAPI specification for the VDB API. + +Examples: + vulnetix vdb spec + vulnetix vdb spec --output json > vdb-spec.json`, + RunE: func(cmd *cobra.Command, args []string) error { + client := vdb.NewClient(vdbOrgID, vdbSecretKey) + if vdbBaseURL != "" { + client.BaseURL = vdbBaseURL + } + + fmt.Println("📋 Fetching OpenAPI specification...") + + spec, err := client.GetOpenAPISpec() + if err != nil { + return fmt.Errorf("failed to get spec: %w", err) + } + + return printOutput(spec, vdbOutput) + }, +} + +// printOutput prints the output in the specified format +func printOutput(data interface{}, format string) error { + switch format { + case "json": + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + return encoder.Encode(data) + case "pretty", "": + jsonBytes, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("failed to format output: %w", err) + } + fmt.Println(string(jsonBytes)) + return nil + default: + return fmt.Errorf("unsupported output format: %s", format) + } +} + +func init() { + // Add vdb command to root + rootCmd.AddCommand(vdbCmd) + + // Add subcommands + vdbCmd.AddCommand(cveCmd) + vdbCmd.AddCommand(ecosystemsCmd) + vdbCmd.AddCommand(productCmd) + vdbCmd.AddCommand(vulnsCmd) + vdbCmd.AddCommand(specCmd) + + // Global flags + vdbCmd.PersistentFlags().StringVar(&vdbOrgID, "org-id", "", "Organization UUID (overrides VVD_ORG env var)") + vdbCmd.PersistentFlags().StringVar(&vdbSecretKey, "secret", "", "Secret key (overrides VVD_SECRET env var)") + vdbCmd.PersistentFlags().StringVar(&vdbBaseURL, "base-url", vdb.DefaultBaseURL, "VDB API base URL") + vdbCmd.PersistentFlags().StringVarP(&vdbOutput, "output", "o", "pretty", "Output format (json, pretty)") + + // Pagination flags for applicable commands + productCmd.Flags().IntVar(&vdbLimit, "limit", 100, "Maximum number of results to return") + productCmd.Flags().IntVar(&vdbOffset, "offset", 0, "Number of results to skip") + + vulnsCmd.Flags().IntVar(&vdbLimit, "limit", 100, "Maximum number of results to return") + vulnsCmd.Flags().IntVar(&vdbOffset, "offset", 0, "Number of results to skip") +} diff --git a/docs/VDB-COMMAND.md b/docs/VDB-COMMAND.md new file mode 100644 index 0000000..0ab0ee8 --- /dev/null +++ b/docs/VDB-COMMAND.md @@ -0,0 +1,424 @@ +# VDB Command Reference + +The `vdb` subcommand provides access to the Vulnetix Vulnerability Database (VDB) API, offering comprehensive vulnerability intelligence from multiple authoritative sources. + +## Table of Contents + +- [Overview](#overview) +- [Authentication](#authentication) +- [Commands](#commands) + - [vdb cve](#vdb-cve) + - [vdb ecosystems](#vdb-ecosystems) + - [vdb product](#vdb-product) + - [vdb vulns](#vdb-vulns) + - [vdb spec](#vdb-spec) +- [Examples](#examples) +- [Rate Limiting](#rate-limiting) + +## Overview + +The VDB API aggregates vulnerability data from: + +- **Primary Sources**: MITRE CVE, NIST NVD, CISA KEV +- **Enhanced Intelligence**: VulnCheck KEV/NVD++/XDB, CrowdSec +- **Ecosystem Sources**: GitHub Security Advisories, OSV, EUVD +- **Risk Scoring**: FIRST EPSS, Coalition CESS + +## Authentication + +### Environment Variables + +The recommended approach is to set environment variables: + +```bash +export VVD_ORG="your-organization-uuid" +export VVD_SECRET="your-secret-key" +``` + +### Configuration File + +Alternatively, create `~/.vulnetix/vdb.json`: + +```json +{ + "org_id": "your-organization-uuid", + "secret_key": "your-secret-key" +} +``` + +### Command-Line Flags + +You can also provide credentials via flags (not recommended for security): + +```bash +vulnetix vdb ecosystems --org-id "your-uuid" --secret "your-secret" +``` + +### Obtaining Credentials + +1. **Via Demo Request**: Visit https://www.vulnetix.com and complete the demo request form +2. **Via Email**: Send a request to sales@vulnetix.com with subject "VDB API Access Request" + +## Commands + +### vdb cve + +Retrieve detailed information about a specific CVE. + +**Usage:** +```bash +vulnetix vdb cve [flags] +``` + +**Flags:** +- `-o, --output string`: Output format (json, pretty) (default "pretty") + +**Examples:** +```bash +# Get CVE information +vulnetix vdb cve CVE-2024-1234 + +# Get CVE in JSON format +vulnetix vdb cve CVE-2024-1234 --output json + +# Save CVE to file +vulnetix vdb cve CVE-2024-1234 -o json > cve-2024-1234.json +``` + +**Response includes:** +- CVE identifier +- Description +- Published and modified dates +- CVSS scores (v2, v3, v4 where available) +- References and advisories +- Affected products and versions +- EPSS probability scores +- KEV (Known Exploited Vulnerabilities) status + +--- + +### vdb ecosystems + +List all available package ecosystems in the VDB. + +**Usage:** +```bash +vulnetix vdb ecosystems [flags] +``` + +**Flags:** +- `-o, --output string`: Output format (json, pretty) (default "pretty") + +**Examples:** +```bash +# List ecosystems +vulnetix vdb ecosystems + +# Get ecosystems as JSON +vulnetix vdb ecosystems --output json +``` + +**Typical ecosystems include:** +- npm (JavaScript/Node.js) +- PyPI (Python) +- Maven (Java) +- Go +- RubyGems +- NuGet (.NET) +- crates.io (Rust) +- And many more... + +--- + +### vdb product + +Get version information for a specific product or package. + +**Usage:** +```bash +vulnetix vdb product [version] [flags] +``` + +**Flags:** +- `--limit int`: Maximum number of results to return (default 100) +- `--offset int`: Number of results to skip (default 0) +- `-o, --output string`: Output format (json, pretty) (default "pretty") + +**Examples:** +```bash +# List all versions of a product +vulnetix vdb product express + +# Get specific version information +vulnetix vdb product express 4.17.1 + +# List with pagination +vulnetix vdb product express --limit 50 --offset 100 + +# Get all versions as JSON +vulnetix vdb product lodash --output json +``` + +**List response includes:** +- Package/product name +- Total number of versions +- Array of version strings +- Pagination information (hasMore, limit, offset) + +**Specific version response includes:** +- Detailed version metadata +- Dependencies +- Known vulnerabilities +- Release date +- Maintainer information + +--- + +### vdb vulns + +Retrieve all known vulnerabilities for a specific package. + +**Usage:** +```bash +vulnetix vdb vulns [flags] +``` + +**Flags:** +- `--limit int`: Maximum number of results to return (default 100) +- `--offset int`: Number of results to skip (default 0) +- `-o, --output string`: Output format (json, pretty) (default "pretty") + +**Examples:** +```bash +# Get vulnerabilities for a package +vulnetix vdb vulns express + +# Get vulnerabilities with pagination +vulnetix vdb vulns lodash --limit 20 + +# Get vulnerabilities as JSON +vulnetix vdb vulns moment --output json + +# Get next page of results +vulnetix vdb vulns react --offset 100 +``` + +**Response includes:** +- Total vulnerability count +- Array of vulnerabilities with: + - CVE identifiers + - Severity levels + - CVSS scores + - Affected version ranges + - Fixed versions + - Descriptions + - References +- Pagination information + +--- + +### vdb spec + +Retrieve the OpenAPI specification for the VDB API. + +**Usage:** +```bash +vulnetix vdb spec [flags] +``` + +**Flags:** +- `-o, --output string`: Output format (json, pretty) (default "pretty") + +**Examples:** +```bash +# View the API specification +vulnetix vdb spec + +# Save specification to file +vulnetix vdb spec --output json > vdb-openapi-spec.json + +# Use with other tools +vulnetix vdb spec -o json | jq '.paths' +``` + +--- + +## Examples + +### Check a Specific CVE + +```bash +# Get information about Log4Shell +vulnetix vdb cve CVE-2021-44228 +``` + +### Audit Package Vulnerabilities + +```bash +# Check if Express.js has vulnerabilities +vulnetix vdb vulns express + +# Check specific version +vulnetix vdb product express 4.16.0 +``` + +### Explore Available Data + +```bash +# List all ecosystems +vulnetix vdb ecosystems + +# Find all versions of a package +vulnetix vdb product react --limit 500 +``` + +### Export Data for Analysis + +```bash +# Export all CVE data +vulnetix vdb cve CVE-2024-1234 -o json > analysis/cve-2024-1234.json + +# Export all vulnerabilities for a package +vulnetix vdb vulns webpack -o json > reports/webpack-vulns.json + +# Export API specification +vulnetix vdb spec -o json > docs/vdb-api-spec.json +``` + +### Combine with Other Tools + +```bash +# Filter CVE data with jq +vulnetix vdb cve CVE-2024-1234 -o json | jq '.cvss.v3.baseScore' + +# Count vulnerabilities +vulnetix vdb vulns lodash -o json | jq '.total' + +# Extract severity levels +vulnetix vdb vulns express -o json | jq '.vulnerabilities[].severity' | sort | uniq -c +``` + +## Rate Limiting + +The VDB API implements rate limiting to ensure fair usage: + +### Per-Minute Rate Limit +- **Default**: 60 requests per minute +- Exceeded requests receive HTTP 429 status + +### Weekly Quota +- **Default**: 1000 requests per week (configurable per organization) +- Resets every Sunday at 00:00 UTC +- Contact sales@vulnetix.com for higher quotas + +### Rate Limit Headers + +All responses include rate limit information: + +``` +RateLimit-MinuteLimit: 60 +RateLimit-Remaining: 45 +RateLimit-Reset: 28 +RateLimit-WeekLimit: 10000 +RateLimit-WeekRemaining: 8543 +RateLimit-WeekReset: 172800 +``` + +### Handling Rate Limits + +The CLI automatically handles token expiration (15-minute JWT tokens). + +For rate limit errors, the API returns: +```json +{ + "success": false, + "error": "Rate limit exceeded", + "details": "Too many requests. Limit: 60 requests per minute. Try again in 42 seconds." +} +``` + +**Best Practices:** +- Cache responses when possible +- Use pagination parameters to reduce request count +- Implement exponential backoff for retries +- Monitor rate limit headers +- Contact Vulnetix for production usage quotas + +## Global Flags + +All `vdb` commands support these global flags: + +- `--org-id string`: Organization UUID (overrides VVD_ORG env var) +- `--secret string`: Secret key (overrides VVD_SECRET env var) +- `--base-url string`: VDB API base URL (default "https://api.vdb.vulnetix.com/v1") +- `-o, --output string`: Output format (json, pretty) (default "pretty") + +## Security Notes + +1. **Never commit credentials** to version control +2. **Use environment variables** or secure configuration files +3. **Rotate secrets regularly** for production use +4. **Store secrets securely** using secrets managers in CI/CD +5. **Limit access** to credentials on shared systems + +## Troubleshooting + +### Authentication Errors + +```bash +# Error: credentials not found +# Solution: Set environment variables +export VVD_ORG="your-uuid" +export VVD_SECRET="your-secret" + +# Or create config file +mkdir -p ~/.vulnetix +cat > ~/.vulnetix/vdb.json << EOF +{ + "org_id": "your-uuid", + "secret_key": "your-secret" +} +EOF +``` + +### Token Expiration + +JWT tokens automatically expire after 15 minutes. The CLI handles token refresh automatically. If you encounter token errors, try: + +```bash +# The CLI will automatically request a new token +vulnetix vdb ecosystems +``` + +### Rate Limiting + +If you exceed rate limits: + +1. Wait for the reset time indicated in the error message +2. Consider implementing caching +3. Use pagination to reduce request frequency +4. Contact Vulnetix for higher quotas + +### Network Issues + +```bash +# Test connectivity +curl -I https://api.vdb.vulnetix.com/v1/spec + +# Use custom base URL if needed +vulnetix vdb ecosystems --base-url https://custom-endpoint.example.com/v1 +``` + +## API Documentation + +For complete API documentation, visit: +- OpenAPI Spec: https://api.vdb.vulnetix.com/v1/spec +- Interactive Docs: https://redocly.github.io/redoc/?url=https://api.vdb.vulnetix.com/v1/spec +- User Guide: Contact sales@vulnetix.com + +## Support + +For assistance: +- Email: sales@vulnetix.com +- Website: https://www.vulnetix.com +- GitHub Issues: https://github.com/vulnetix/cli/issues diff --git a/docs/VDB-QUICKSTART.md b/docs/VDB-QUICKSTART.md new file mode 100644 index 0000000..0a51a53 --- /dev/null +++ b/docs/VDB-QUICKSTART.md @@ -0,0 +1,296 @@ +# VDB Quick Start Guide + +Get started with the Vulnetix Vulnerability Database (VDB) CLI in minutes. + +## Prerequisites + +- Vulnetix CLI installed +- VDB API credentials (Organization UUID and Secret Key) + +## 1. Obtain Credentials + +### Option A: Request via Website +Visit https://www.vulnetix.com and complete the demo request form. + +### Option B: Email Request +Send an email to sales@vulnetix.com with: +- Subject: "VDB API Access Request" +- Include: Company name, use case, and contact information + +You'll receive: +- **Organization UUID**: Format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` +- **Secret Key**: 64-character alphanumeric string + +## 2. Configure Credentials + +### Method 1: Environment Variables (Recommended) + +```bash +# Add to your ~/.bashrc, ~/.zshrc, or ~/.profile +export VVD_ORG="your-organization-uuid" +export VVD_SECRET="your-64-character-secret-key" + +# Reload your shell configuration +source ~/.bashrc # or ~/.zshrc +``` + +### Method 2: Configuration File + +```bash +# Create configuration directory +mkdir -p ~/.vulnetix + +# Create configuration file +cat > ~/.vulnetix/vdb.json << 'EOF' +{ + "org_id": "your-organization-uuid", + "secret_key": "your-64-character-secret-key" +} +EOF + +# Secure the file +chmod 600 ~/.vulnetix/vdb.json +``` + +## 3. Verify Setup + +```bash +# Test your credentials by listing ecosystems +vulnetix vdb ecosystems +``` + +Expected output: +``` +🌐 Fetching available ecosystems... + +✅ Found 50+ ecosystems: + + • npm + • PyPI + • Maven + • Go + • RubyGems + ... +``` + +## 4. Try Your First Queries + +### Check a Famous CVE + +```bash +# Get information about Log4Shell +vulnetix vdb cve CVE-2021-44228 +``` + +### Find Package Vulnerabilities + +```bash +# Check Express.js vulnerabilities +vulnetix vdb vulns express +``` + +### List Product Versions + +```bash +# List all versions of React +vulnetix vdb product react --limit 20 +``` + +## Common Use Cases + +### Security Audit Workflow + +```bash +# 1. List all vulnerabilities for your package +vulnetix vdb vulns lodash -o json > audit-results.json + +# 2. Check specific CVEs +vulnetix vdb cve CVE-2024-1234 + +# 3. Verify if specific version is affected +vulnetix vdb product lodash 4.17.20 +``` + +### CI/CD Integration + +```bash +#!/bin/bash +# check-vulnerabilities.sh + +PACKAGE_NAME="express" +PACKAGE_VERSION="4.17.1" + +# Check for vulnerabilities +VULNS=$(vulnetix vdb vulns $PACKAGE_NAME -o json) +COUNT=$(echo "$VULNS" | jq '.total') + +if [ "$COUNT" -gt 0 ]; then + echo "⚠️ Found $COUNT vulnerabilities in $PACKAGE_NAME" + exit 1 +else + echo "✅ No vulnerabilities found" + exit 0 +fi +``` + +### Bulk CVE Checking + +```bash +#!/bin/bash +# check-cves.sh + +# Read CVEs from file (one per line) +while IFS= read -r cve; do + echo "Checking $cve..." + vulnetix vdb cve "$cve" -o json > "reports/${cve}.json" + sleep 1 # Rate limiting +done < cve-list.txt +``` + +## Next Steps + +1. **Read the Full Documentation**: See [VDB-COMMAND.md](./VDB-COMMAND.md) +2. **Explore the API**: `vulnetix vdb spec -o json > api-spec.json` +3. **Automate Checks**: Integrate VDB into your CI/CD pipeline +4. **Monitor Updates**: Subscribe to vulnerability feeds + +## Troubleshooting + +### "credentials not found" Error + +**Problem**: CLI can't find your credentials. + +**Solution**: +```bash +# Verify environment variables are set +echo $VVD_ORG +echo $VVD_SECRET + +# Or check config file exists +cat ~/.vulnetix/vdb.json +``` + +### "Invalid signature" Error + +**Problem**: Credentials are incorrect or malformed. + +**Solution**: +- Verify your Organization UUID is a valid UUID format +- Ensure Secret Key is exactly 64 characters +- Check for extra spaces or newlines in credentials +- Request new credentials if needed + +### "Rate limit exceeded" Error + +**Problem**: Too many requests in a short time. + +**Solution**: +- Wait for the reset time shown in the error message +- Default: 60 requests per minute, 1000 per week +- Add delays between requests in scripts +- Contact sales@vulnetix.com for higher quotas + +### "Token has expired" Error + +**Problem**: JWT token expired (15-minute lifetime). + +**Solution**: +- The CLI automatically refreshes tokens +- This error usually self-resolves on retry +- If persistent, check system clock synchronization + +## Advanced Tips + +### Use Aliases + +```bash +# Add to ~/.bashrc or ~/.zshrc +alias vdb='vulnetix vdb' +alias vdb-cve='vulnetix vdb cve' +alias vdb-vulns='vulnetix vdb vulns' + +# Now use shorter commands +vdb-cve CVE-2024-1234 +vdb-vulns express +``` + +### Combine with jq + +```bash +# Extract CVSS base score +vulnetix vdb cve CVE-2024-1234 -o json | jq '.cvss.v3.baseScore' + +# Get high severity vulns only +vulnetix vdb vulns lodash -o json | \ + jq '.vulnerabilities[] | select(.severity == "HIGH")' + +# Count vulnerabilities by severity +vulnetix vdb vulns webpack -o json | \ + jq -r '.vulnerabilities[].severity' | \ + sort | uniq -c +``` + +### Cache Results + +```bash +# Cache ecosystems list +vulnetix vdb ecosystems -o json > ~/.vulnetix/cache/ecosystems.json + +# Use cached data +cat ~/.vulnetix/cache/ecosystems.json | jq +``` + +### Create Reports + +```bash +#!/bin/bash +# generate-report.sh + +PACKAGES=("express" "lodash" "react" "axios") +REPORT_DIR="vulnerability-report-$(date +%Y%m%d)" +mkdir -p "$REPORT_DIR" + +for pkg in "${PACKAGES[@]}"; do + echo "Scanning $pkg..." + vulnetix vdb vulns "$pkg" -o json > "$REPORT_DIR/${pkg}-vulns.json" +done + +echo "Report generated in $REPORT_DIR/" +``` + +## Support Resources + +- **Documentation**: See [VDB-COMMAND.md](./VDB-COMMAND.md) +- **API Spec**: https://api.vdb.vulnetix.com/v1/spec +- **Email Support**: sales@vulnetix.com +- **Website**: https://www.vulnetix.com + +## Security Best Practices + +1. **Never commit credentials** to Git + ```bash + # Add to .gitignore + echo ".vulnetix/" >> .gitignore + echo "vdb.json" >> .gitignore + ``` + +2. **Use environment variables in CI/CD** + ```yaml + # GitHub Actions example + - name: Check vulnerabilities + env: + VVD_ORG: ${{ secrets.VVD_ORG }} + VVD_SECRET: ${{ secrets.VVD_SECRET }} + run: vulnetix vdb vulns ${{ matrix.package }} + ``` + +3. **Rotate credentials regularly** for production use + +4. **Limit credential access** on shared systems + ```bash + chmod 600 ~/.vulnetix/vdb.json + ``` + +--- + +**Ready to go?** Start with `vulnetix vdb ecosystems` and explore! diff --git a/examples/vdb-ci-example.sh b/examples/vdb-ci-example.sh new file mode 100644 index 0000000..423d065 --- /dev/null +++ b/examples/vdb-ci-example.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# +# Example: VDB Vulnerability Scanning in CI/CD +# +# This script demonstrates how to integrate VDB API into your CI/CD pipeline +# to check for vulnerabilities in your dependencies. +# +# Usage: +# ./vdb-ci-example.sh [severity-threshold] +# +# Environment Variables Required: +# VVD_ORG - Your organization UUID +# VVD_SECRET - Your secret key +# +# Exit Codes: +# 0 - No vulnerabilities found or below threshold +# 1 - Vulnerabilities found above threshold +# 2 - Error occurred + +set -e + +PACKAGE_NAME="${1:-}" +SEVERITY_THRESHOLD="${2:-HIGH}" # CRITICAL, HIGH, MEDIUM, LOW + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Validate inputs +if [ -z "$PACKAGE_NAME" ]; then + echo "Usage: $0 [severity-threshold]" + echo "Example: $0 express HIGH" + exit 2 +fi + +# Check credentials +if [ -z "$VVD_ORG" ] || [ -z "$VVD_SECRET" ]; then + echo -e "${RED}Error: VVD_ORG and VVD_SECRET environment variables must be set${NC}" + exit 2 +fi + +echo "🔍 Scanning $PACKAGE_NAME for vulnerabilities..." +echo "📊 Severity threshold: $SEVERITY_THRESHOLD" +echo "" + +# Fetch vulnerabilities +VULNS_JSON=$(vulnetix vdb vulns "$PACKAGE_NAME" -o json 2>&1) + +if [ $? -ne 0 ]; then + echo -e "${RED}❌ Failed to fetch vulnerabilities${NC}" + echo "$VULNS_JSON" + exit 2 +fi + +# Parse results +TOTAL=$(echo "$VULNS_JSON" | jq -r '.total') +CRITICAL=$(echo "$VULNS_JSON" | jq '[.vulnerabilities[] | select(.severity == "CRITICAL")] | length') +HIGH=$(echo "$VULNS_JSON" | jq '[.vulnerabilities[] | select(.severity == "HIGH")] | length') +MEDIUM=$(echo "$VULNS_JSON" | jq '[.vulnerabilities[] | select(.severity == "MEDIUM")] | length') +LOW=$(echo "$VULNS_JSON" | jq '[.vulnerabilities[] | select(.severity == "LOW")] | length') + +# Display summary +echo "📋 Vulnerability Summary:" +echo " Total: $TOTAL" +echo -e " ${RED}Critical: $CRITICAL${NC}" +echo -e " ${YELLOW}High: $HIGH${NC}" +echo " Medium: $MEDIUM" +echo " Low: $LOW" +echo "" + +# Determine if build should fail +FAIL_BUILD=false + +case "$SEVERITY_THRESHOLD" in + CRITICAL) + if [ "$CRITICAL" -gt 0 ]; then + FAIL_BUILD=true + fi + ;; + HIGH) + if [ "$CRITICAL" -gt 0 ] || [ "$HIGH" -gt 0 ]; then + FAIL_BUILD=true + fi + ;; + MEDIUM) + if [ "$CRITICAL" -gt 0 ] || [ "$HIGH" -gt 0 ] || [ "$MEDIUM" -gt 0 ]; then + FAIL_BUILD=true + fi + ;; + LOW) + if [ "$TOTAL" -gt 0 ]; then + FAIL_BUILD=true + fi + ;; +esac + +if [ "$FAIL_BUILD" = true ]; then + echo -e "${RED}❌ Build failed: Vulnerabilities found above threshold${NC}" + echo "" + echo "Critical/High vulnerabilities:" + echo "$VULNS_JSON" | jq -r '.vulnerabilities[] | select(.severity == "CRITICAL" or .severity == "HIGH") | " - \(.id // .cve): \(.severity)"' + exit 1 +else + echo -e "${GREEN}✅ Build passed: No vulnerabilities above threshold${NC}" + exit 0 +fi diff --git a/examples/vdb-config.json b/examples/vdb-config.json new file mode 100644 index 0000000..25d0d36 --- /dev/null +++ b/examples/vdb-config.json @@ -0,0 +1,4 @@ +{ + "org_id": "00000000-0000-0000-0000-000000000000", + "secret_key": "0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/examples/vdb-github-action.yml b/examples/vdb-github-action.yml new file mode 100644 index 0000000..88aa235 --- /dev/null +++ b/examples/vdb-github-action.yml @@ -0,0 +1,158 @@ +# Example GitHub Actions workflow for VDB vulnerability scanning +# +# This workflow demonstrates how to use the Vulnetix CLI VDB command +# in GitHub Actions to scan dependencies for vulnerabilities. +# +# Setup: +# 1. Add VVD_ORG and VVD_SECRET to your repository secrets +# 2. Customize the matrix to include your packages +# 3. Adjust the severity threshold as needed + +name: VDB Vulnerability Scan + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + schedule: + # Run daily at 2 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: + +jobs: + scan-dependencies: + name: Scan ${{ matrix.package }} for vulnerabilities + runs-on: ubuntu-latest + + strategy: + matrix: + package: + - express + - lodash + - axios + - react + - webpack + fail-fast: false + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Vulnetix CLI + run: | + # Install from GitHub releases (adjust version as needed) + curl -LO https://github.com/vulnetix/cli/releases/latest/download/vulnetix-linux-amd64 + chmod +x vulnetix-linux-amd64 + sudo mv vulnetix-linux-amd64 /usr/local/bin/vulnetix + vulnetix version + + - name: Verify VDB credentials + env: + VVD_ORG: ${{ secrets.VVD_ORG }} + VVD_SECRET: ${{ secrets.VVD_SECRET }} + run: | + if [ -z "$VVD_ORG" ] || [ -z "$VVD_SECRET" ]; then + echo "Error: VVD_ORG and VVD_SECRET secrets must be set" + exit 1 + fi + echo "Credentials verified" + + - name: Scan for vulnerabilities + id: scan + env: + VVD_ORG: ${{ secrets.VVD_ORG }} + VVD_SECRET: ${{ secrets.VVD_SECRET }} + run: | + echo "Scanning ${{ matrix.package }}..." + + # Fetch vulnerabilities + vulnetix vdb vulns "${{ matrix.package }}" -o json > vulns.json + + # Parse results + TOTAL=$(jq '.total' vulns.json) + CRITICAL=$(jq '[.vulnerabilities[] | select(.severity == "CRITICAL")] | length' vulns.json) + HIGH=$(jq '[.vulnerabilities[] | select(.severity == "HIGH")] | length' vulns.json) + + echo "total=$TOTAL" >> $GITHUB_OUTPUT + echo "critical=$CRITICAL" >> $GITHUB_OUTPUT + echo "high=$HIGH" >> $GITHUB_OUTPUT + + echo "Total vulnerabilities: $TOTAL" + echo "Critical: $CRITICAL" + echo "High: $HIGH" + + - name: Upload vulnerability report + uses: actions/upload-artifact@v4 + if: always() + with: + name: vulnerability-report-${{ matrix.package }} + path: vulns.json + retention-days: 30 + + - name: Check vulnerability threshold + if: steps.scan.outputs.critical > 0 || steps.scan.outputs.high > 0 + run: | + echo "❌ Critical or High severity vulnerabilities found!" + echo "Critical: ${{ steps.scan.outputs.critical }}" + echo "High: ${{ steps.scan.outputs.high }}" + exit 1 + + - name: Create issue if vulnerabilities found + if: failure() && github.event_name == 'schedule' + uses: actions/github-script@v7 + with: + script: | + const total = '${{ steps.scan.outputs.total }}'; + const critical = '${{ steps.scan.outputs.critical }}'; + const high = '${{ steps.scan.outputs.high }}'; + const package = '${{ matrix.package }}'; + + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `🚨 Vulnerabilities found in ${package}`, + body: `## Vulnerability Scan Results\n\n` + + `**Package:** ${package}\n` + + `**Total vulnerabilities:** ${total}\n` + + `**Critical:** ${critical}\n` + + `**High:** ${high}\n\n` + + `Please review the vulnerability report and take action.\n\n` + + `[View workflow run](${context.payload.repository.html_url}/actions/runs/${context.runId})`, + labels: ['security', 'vulnerability'] + }); + + generate-summary: + name: Generate Security Summary + needs: scan-dependencies + runs-on: ubuntu-latest + if: always() + + steps: + - name: Download all reports + uses: actions/download-artifact@v4 + with: + path: reports + + - name: Generate summary + run: | + echo "## 🔒 VDB Vulnerability Scan Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Package | Total | Critical | High | Medium | Low |" >> $GITHUB_STEP_SUMMARY + echo "|---------|-------|----------|------|--------|-----|" >> $GITHUB_STEP_SUMMARY + + for report in reports/*/vulns.json; do + if [ -f "$report" ]; then + pkg=$(jq -r '.packageName' "$report") + total=$(jq '.total' "$report") + critical=$(jq '[.vulnerabilities[] | select(.severity == "CRITICAL")] | length' "$report") + high=$(jq '[.vulnerabilities[] | select(.severity == "HIGH")] | length' "$report") + medium=$(jq '[.vulnerabilities[] | select(.severity == "MEDIUM")] | length' "$report") + low=$(jq '[.vulnerabilities[] | select(.severity == "LOW")] | length' "$report") + + echo "| $pkg | $total | $critical | $high | $medium | $low |" >> $GITHUB_STEP_SUMMARY + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + echo "📊 Scan completed on $(date)" >> $GITHUB_STEP_SUMMARY diff --git a/internal/vdb/api.go b/internal/vdb/api.go new file mode 100644 index 0000000..d336f8a --- /dev/null +++ b/internal/vdb/api.go @@ -0,0 +1,176 @@ +package vdb + +import ( + "encoding/json" + "fmt" + "net/url" +) + +// CVEInfo represents vulnerability information for a CVE +type CVEInfo struct { + CVE string `json:"cve"` + Description string `json:"description,omitempty"` + Published string `json:"published,omitempty"` + Modified string `json:"modified,omitempty"` + CVSS map[string]interface{} `json:"cvss,omitempty"` + References []interface{} `json:"references,omitempty"` + Data map[string]interface{} `json:"-"` // Store full response +} + +// EcosystemsResponse represents the ecosystems list response +type EcosystemsResponse struct { + Ecosystems []string `json:"ecosystems"` +} + +// ProductVersionsResponse represents product versions with pagination +type ProductVersionsResponse struct { + PackageName string `json:"packageName"` + Timestamp int64 `json:"timestamp"` + Total int `json:"total"` + Limit int `json:"limit"` + Offset int `json:"offset"` + HasMore bool `json:"hasMore"` + Versions []string `json:"versions"` +} + +// VulnerabilitiesResponse represents vulnerabilities for a package +type VulnerabilitiesResponse struct { + PackageName string `json:"packageName"` + Timestamp int64 `json:"timestamp"` + Total int `json:"total"` + Limit int `json:"limit"` + Offset int `json:"offset"` + HasMore bool `json:"hasMore"` + Vulnerabilities []map[string]interface{} `json:"vulnerabilities"` +} + +// GetCVE retrieves information about a specific CVE +func (c *Client) GetCVE(cveID string) (*CVEInfo, error) { + path := fmt.Sprintf("/info/%s", cveID) + + respBody, err := c.DoRequest("GET", path, nil) + if err != nil { + return nil, err + } + + var cveInfo CVEInfo + if err := json.Unmarshal(respBody, &cveInfo); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + // Store full response in Data field + var fullData map[string]interface{} + json.Unmarshal(respBody, &fullData) + cveInfo.Data = fullData + + return &cveInfo, nil +} + +// GetEcosystems retrieves the list of available ecosystems +func (c *Client) GetEcosystems() ([]string, error) { + path := "/ecosystems" + + respBody, err := c.DoRequest("GET", path, nil) + if err != nil { + return nil, err + } + + var resp EcosystemsResponse + if err := json.Unmarshal(respBody, &resp); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return resp.Ecosystems, nil +} + +// GetProductVersions retrieves all versions for a product with pagination +func (c *Client) GetProductVersions(productName string, limit, offset int) (*ProductVersionsResponse, error) { + path := fmt.Sprintf("/product/%s", url.PathEscape(productName)) + + // Add pagination parameters + if limit > 0 || offset > 0 { + params := url.Values{} + if limit > 0 { + params.Add("limit", fmt.Sprintf("%d", limit)) + } + if offset > 0 { + params.Add("offset", fmt.Sprintf("%d", offset)) + } + path = path + "?" + params.Encode() + } + + respBody, err := c.DoRequest("GET", path, nil) + if err != nil { + return nil, err + } + + var resp ProductVersionsResponse + if err := json.Unmarshal(respBody, &resp); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &resp, nil +} + +// GetProductVersion retrieves information for a specific product version +func (c *Client) GetProductVersion(productName, version string) (map[string]interface{}, error) { + path := fmt.Sprintf("/product/%s/%s", url.PathEscape(productName), url.PathEscape(version)) + + respBody, err := c.DoRequest("GET", path, nil) + if err != nil { + return nil, err + } + + var result map[string]interface{} + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return result, nil +} + +// GetPackageVulnerabilities retrieves vulnerabilities for a package +func (c *Client) GetPackageVulnerabilities(packageName string, limit, offset int) (*VulnerabilitiesResponse, error) { + path := fmt.Sprintf("/%s/vulns", url.PathEscape(packageName)) + + // Add pagination parameters + if limit > 0 || offset > 0 { + params := url.Values{} + if limit > 0 { + params.Add("limit", fmt.Sprintf("%d", limit)) + } + if offset > 0 { + params.Add("offset", fmt.Sprintf("%d", offset)) + } + path = path + "?" + params.Encode() + } + + respBody, err := c.DoRequest("GET", path, nil) + if err != nil { + return nil, err + } + + var resp VulnerabilitiesResponse + if err := json.Unmarshal(respBody, &resp); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &resp, nil +} + +// GetOpenAPISpec retrieves the OpenAPI specification +func (c *Client) GetOpenAPISpec() (map[string]interface{}, error) { + path := "/spec" + + respBody, err := c.DoRequest("GET", path, nil) + if err != nil { + return nil, err + } + + var spec map[string]interface{} + if err := json.Unmarshal(respBody, &spec); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return spec, nil +} diff --git a/internal/vdb/client.go b/internal/vdb/client.go new file mode 100644 index 0000000..b2cd3da --- /dev/null +++ b/internal/vdb/client.go @@ -0,0 +1,288 @@ +package vdb + +import ( + "bytes" + "crypto/hmac" + "crypto/sha512" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" +) + +const ( + DefaultBaseURL = "https://api.vdb.vulnetix.com/v1" + Region = "us-east-1" + Service = "vdb" + Algorithm = "AWS4-HMAC-SHA512" + TokenExpiry = 15 * time.Minute +) + +// Client represents a VDB API client +type Client struct { + BaseURL string + OrgID string + SecretKey string + HTTPClient *http.Client + token *TokenCache +} + +// TokenCache stores the JWT token and its expiration +type TokenCache struct { + Token string + ExpiresAt time.Time +} + +// TokenResponse represents the JWT token response +type TokenResponse struct { + Token string `json:"token"` + Iss string `json:"iss"` + Sub string `json:"sub"` + Exp int64 `json:"exp"` +} + +// ErrorResponse represents an API error response +type ErrorResponse struct { + Success bool `json:"success"` + Error string `json:"error"` + Details string `json:"details,omitempty"` +} + +// NewClient creates a new VDB API client +func NewClient(orgID, secretKey string) *Client { + return &Client{ + BaseURL: DefaultBaseURL, + OrgID: orgID, + SecretKey: secretKey, + HTTPClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// GetToken retrieves a valid JWT token (from cache or by requesting a new one) +func (c *Client) GetToken() (string, error) { + // Check if we have a valid cached token + if c.token != nil && time.Now().Before(c.token.ExpiresAt.Add(-1*time.Minute)) { + return c.token.Token, nil + } + + // Request a new token + return c.requestNewToken() +} + +// requestNewToken requests a new JWT token using AWS SigV4 authentication +func (c *Client) requestNewToken() (string, error) { + path := "/auth/token" + url := c.BaseURL + path + + // Create the request + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + // Sign the request with AWS SigV4 + if err := c.signRequest(req, path, ""); err != nil { + return "", fmt.Errorf("failed to sign request: %w", err) + } + + // Execute the request + resp, err := c.HTTPClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to execute request: %w", err) + } + defer resp.Body.Close() + + // Read the response + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + // Check for errors + if resp.StatusCode != http.StatusOK { + var errResp ErrorResponse + if err := json.Unmarshal(body, &errResp); err == nil { + return "", fmt.Errorf("API error (%d): %s - %s", resp.StatusCode, errResp.Error, errResp.Details) + } + return "", fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body)) + } + + // Parse the response + var tokenResp TokenResponse + if err := json.Unmarshal(body, &tokenResp); err != nil { + return "", fmt.Errorf("failed to parse response: %w", err) + } + + // Cache the token + c.token = &TokenCache{ + Token: tokenResp.Token, + ExpiresAt: time.Unix(tokenResp.Exp, 0), + } + + return tokenResp.Token, nil +} + +// signRequest signs an HTTP request using AWS Signature Version 4 (SHA-512) +func (c *Client) signRequest(req *http.Request, path, body string) error { + now := time.Now().UTC() + amzDate := now.Format("20060102T150405Z") + dateStamp := now.Format("20060102") + + // Add the X-Amz-Date header + req.Header.Set("X-Amz-Date", amzDate) + + // Calculate payload hash + payloadHash := sha512Hash(body) + + // Create canonical request + canonicalHeaders := fmt.Sprintf("x-amz-date:%s\n", amzDate) + signedHeaders := "x-amz-date" + + canonicalRequest := fmt.Sprintf("%s\n%s\n\n%s\n%s\n%s", + req.Method, + path, + canonicalHeaders, + signedHeaders, + payloadHash, + ) + + // Create string to sign + credentialScope := fmt.Sprintf("%s/%s/%s/aws4_request", dateStamp, Region, Service) + stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", + Algorithm, + amzDate, + credentialScope, + sha512Hash(canonicalRequest), + ) + + // Calculate signature + signingKey := getSignatureKey(c.SecretKey, dateStamp, Region, Service) + signature := hex.EncodeToString(hmacSHA512(signingKey, stringToSign)) + + // Create authorization header + authHeader := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", + Algorithm, + c.OrgID, + credentialScope, + signedHeaders, + signature, + ) + + req.Header.Set("Authorization", authHeader) + return nil +} + +// DoRequest performs an authenticated API request +func (c *Client) DoRequest(method, path string, body interface{}) ([]byte, error) { + // Get a valid token + token, err := c.GetToken() + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + // Prepare request body + var bodyReader io.Reader + if body != nil { + bodyBytes, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + bodyReader = bytes.NewReader(bodyBytes) + } + + // Create the request + url := c.BaseURL + path + req, err := http.NewRequest(method, url, bodyReader) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Set headers + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + + // Execute the request + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute request: %w", err) + } + defer resp.Body.Close() + + // Read the response + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + // Check for errors + if resp.StatusCode >= 400 { + var errResp ErrorResponse + if err := json.Unmarshal(responseBody, &errResp); err == nil { + return nil, fmt.Errorf("API error (%d): %s - %s", resp.StatusCode, errResp.Error, errResp.Details) + } + return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(responseBody)) + } + + return responseBody, nil +} + +// Helper functions + +func sha512Hash(data string) string { + hash := sha512.Sum512([]byte(data)) + return hex.EncodeToString(hash[:]) +} + +func hmacSHA512(key []byte, data string) []byte { + h := hmac.New(sha512.New, key) + h.Write([]byte(data)) + return h.Sum(nil) +} + +func getSignatureKey(key, dateStamp, region, service string) []byte { + kDate := hmacSHA512([]byte("AWS4"+key), dateStamp) + kRegion := hmacSHA512(kDate, region) + kService := hmacSHA512(kRegion, service) + kSigning := hmacSHA512(kService, "aws4_request") + return kSigning +} + +// LoadCredentials loads VDB credentials from environment or config file +func LoadCredentials() (orgID, secretKey string, err error) { + // Try environment variables first + orgID = os.Getenv("VVD_ORG") + secretKey = os.Getenv("VVD_SECRET") + + if orgID != "" && secretKey != "" { + return orgID, secretKey, nil + } + + // Try loading from config file (~/.vulnetix/vdb.json) + homeDir, err := os.UserHomeDir() + if err != nil { + return "", "", fmt.Errorf("credentials not found in environment. Set VVD_ORG and VVD_SECRET") + } + + configPath := filepath.Join(homeDir, ".vulnetix", "vdb.json") + data, err := os.ReadFile(configPath) + if err != nil { + return "", "", fmt.Errorf("credentials not found. Set VVD_ORG and VVD_SECRET environment variables or create %s", configPath) + } + + var config struct { + OrgID string `json:"org_id"` + SecretKey string `json:"secret_key"` + } + + if err := json.Unmarshal(data, &config); err != nil { + return "", "", fmt.Errorf("failed to parse config file: %w", err) + } + + return config.OrgID, config.SecretKey, nil +} From ecfcd9a20cb3ae9e3e2da9f2b80e7023fff72784 Mon Sep 17 00:00:00 2001 From: Stof <93355168+0x73746F66@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:31:53 +1100 Subject: [PATCH 2/4] Update cmd/vdb.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Stof <93355168+0x73746F66@users.noreply.github.com> --- cmd/vdb.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/vdb.go b/cmd/vdb.go index 52fab58..547df93 100644 --- a/cmd/vdb.go +++ b/cmd/vdb.go @@ -113,7 +113,11 @@ Examples: client.BaseURL = vdbBaseURL } - fmt.Println("🌐 Fetching available ecosystems...") + if vdbOutput == "json" { + fmt.Fprintln(os.Stderr, "🌐 Fetching available ecosystems...") + } else { + fmt.Println("🌐 Fetching available ecosystems...") + } ecosystems, err := client.GetEcosystems() if err != nil { From ff1228b1b9e749896129ad1a56d1435135382374 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:15:26 +0000 Subject: [PATCH 3/4] fix: address code quality issues from PR review - Add mutex for thread safety in VDB client - Increase token validation buffer from 1 to 3 minutes - Fix AWS SigV4 canonical request format (add query string component) - Handle json.Unmarshal error in api.go - Extract pagination logic into helper function - Fix stdout/stderr for JSON output mode (5 locations) - Scope vdbLimit and vdbOffset variables to commands Co-authored-by: 0x73746F66 <93355168+0x73746F66@users.noreply.github.com> --- cmd/vdb.go | 52 +++++++++++++++++++++++++++++++----------- internal/vdb/api.go | 44 ++++++++++++++++++----------------- internal/vdb/client.go | 30 +++++++++++++++++++----- 3 files changed, 86 insertions(+), 40 deletions(-) diff --git a/cmd/vdb.go b/cmd/vdb.go index 547df93..71d15dd 100644 --- a/cmd/vdb.go +++ b/cmd/vdb.go @@ -13,8 +13,6 @@ var ( vdbOrgID string vdbSecretKey string vdbBaseURL string - vdbLimit int - vdbOffset int vdbOutput string ) @@ -87,7 +85,11 @@ Examples: client.BaseURL = vdbBaseURL } - fmt.Printf("🔍 Fetching information for %s...\n", cveID) + if vdbOutput == "json" { + fmt.Fprintf(os.Stderr, "🔍 Fetching information for %s...\n", cveID) + } else { + fmt.Printf("🔍 Fetching information for %s...\n", cveID) + } cveInfo, err := client.GetCVE(cveID) if err != nil { @@ -159,6 +161,10 @@ Examples: RunE: func(cmd *cobra.Command, args []string) error { productName := args[0] + // Get pagination flags + limit, _ := cmd.Flags().GetInt("limit") + offset, _ := cmd.Flags().GetInt("offset") + client := vdb.NewClient(vdbOrgID, vdbSecretKey) if vdbBaseURL != "" { client.BaseURL = vdbBaseURL @@ -167,7 +173,11 @@ Examples: // If version is provided, get specific version info if len(args) > 1 { version := args[1] - fmt.Printf("🔍 Fetching information for %s@%s...\n", productName, version) + if vdbOutput == "json" { + fmt.Fprintf(os.Stderr, "🔍 Fetching information for %s@%s...\n", productName, version) + } else { + fmt.Printf("🔍 Fetching information for %s@%s...\n", productName, version) + } info, err := client.GetProductVersion(productName, version) if err != nil { @@ -178,9 +188,13 @@ Examples: } // Otherwise, list all versions - fmt.Printf("📦 Fetching versions for %s...\n", productName) + if vdbOutput == "json" { + fmt.Fprintf(os.Stderr, "📦 Fetching versions for %s...\n", productName) + } else { + fmt.Printf("📦 Fetching versions for %s...\n", productName) + } - resp, err := client.GetProductVersions(productName, vdbLimit, vdbOffset) + resp, err := client.GetProductVersions(productName, limit, offset) if err != nil { return fmt.Errorf("failed to get product versions: %w", err) } @@ -216,14 +230,22 @@ Examples: RunE: func(cmd *cobra.Command, args []string) error { packageName := args[0] + // Get pagination flags + limit, _ := cmd.Flags().GetInt("limit") + offset, _ := cmd.Flags().GetInt("offset") + client := vdb.NewClient(vdbOrgID, vdbSecretKey) if vdbBaseURL != "" { client.BaseURL = vdbBaseURL } - fmt.Printf("🔒 Fetching vulnerabilities for %s...\n", packageName) + if vdbOutput == "json" { + fmt.Fprintf(os.Stderr, "🔒 Fetching vulnerabilities for %s...\n", packageName) + } else { + fmt.Printf("🔒 Fetching vulnerabilities for %s...\n", packageName) + } - resp, err := client.GetPackageVulnerabilities(packageName, vdbLimit, vdbOffset) + resp, err := client.GetPackageVulnerabilities(packageName, limit, offset) if err != nil { return fmt.Errorf("failed to get vulnerabilities: %w", err) } @@ -272,7 +294,11 @@ Examples: client.BaseURL = vdbBaseURL } - fmt.Println("📋 Fetching OpenAPI specification...") + if vdbOutput == "json" { + fmt.Fprintln(os.Stderr, "📋 Fetching OpenAPI specification...") + } else { + fmt.Println("📋 Fetching OpenAPI specification...") + } spec, err := client.GetOpenAPISpec() if err != nil { @@ -320,9 +346,9 @@ func init() { vdbCmd.PersistentFlags().StringVarP(&vdbOutput, "output", "o", "pretty", "Output format (json, pretty)") // Pagination flags for applicable commands - productCmd.Flags().IntVar(&vdbLimit, "limit", 100, "Maximum number of results to return") - productCmd.Flags().IntVar(&vdbOffset, "offset", 0, "Number of results to skip") + productCmd.Flags().Int("limit", 100, "Maximum number of results to return (default 100; use with --offset for pagination)") + productCmd.Flags().Int("offset", 0, "Number of results to skip (for pagination)") - vulnsCmd.Flags().IntVar(&vdbLimit, "limit", 100, "Maximum number of results to return") - vulnsCmd.Flags().IntVar(&vdbOffset, "offset", 0, "Number of results to skip") + vulnsCmd.Flags().Int("limit", 100, "Maximum number of results to return (default 100; use with --offset for pagination)") + vulnsCmd.Flags().Int("offset", 0, "Number of results to skip (for pagination)") } diff --git a/internal/vdb/api.go b/internal/vdb/api.go index d336f8a..33ed0e1 100644 --- a/internal/vdb/api.go +++ b/internal/vdb/api.go @@ -60,7 +60,9 @@ func (c *Client) GetCVE(cveID string) (*CVEInfo, error) { // Store full response in Data field var fullData map[string]interface{} - json.Unmarshal(respBody, &fullData) + if err := json.Unmarshal(respBody, &fullData); err != nil { + return nil, fmt.Errorf("failed to parse full response data: %w", err) + } cveInfo.Data = fullData return &cveInfo, nil @@ -83,21 +85,30 @@ func (c *Client) GetEcosystems() ([]string, error) { return resp.Ecosystems, nil } +// buildPaginationQuery constructs a query string for pagination parameters. +// It returns an empty string if neither limit nor offset is greater than zero. +func buildPaginationQuery(limit, offset int) string { + if limit <= 0 && offset <= 0 { + return "" + } + + params := url.Values{} + if limit > 0 { + params.Add("limit", fmt.Sprintf("%d", limit)) + } + if offset > 0 { + params.Add("offset", fmt.Sprintf("%d", offset)) + } + + return "?" + params.Encode() +} + // GetProductVersions retrieves all versions for a product with pagination func (c *Client) GetProductVersions(productName string, limit, offset int) (*ProductVersionsResponse, error) { path := fmt.Sprintf("/product/%s", url.PathEscape(productName)) // Add pagination parameters - if limit > 0 || offset > 0 { - params := url.Values{} - if limit > 0 { - params.Add("limit", fmt.Sprintf("%d", limit)) - } - if offset > 0 { - params.Add("offset", fmt.Sprintf("%d", offset)) - } - path = path + "?" + params.Encode() - } + path += buildPaginationQuery(limit, offset) respBody, err := c.DoRequest("GET", path, nil) if err != nil { @@ -134,16 +145,7 @@ func (c *Client) GetPackageVulnerabilities(packageName string, limit, offset int path := fmt.Sprintf("/%s/vulns", url.PathEscape(packageName)) // Add pagination parameters - if limit > 0 || offset > 0 { - params := url.Values{} - if limit > 0 { - params.Add("limit", fmt.Sprintf("%d", limit)) - } - if offset > 0 { - params.Add("offset", fmt.Sprintf("%d", offset)) - } - path = path + "?" + params.Encode() - } + path += buildPaginationQuery(limit, offset) respBody, err := c.DoRequest("GET", path, nil) if err != nil { diff --git a/internal/vdb/client.go b/internal/vdb/client.go index b2cd3da..2338cf0 100644 --- a/internal/vdb/client.go +++ b/internal/vdb/client.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "path/filepath" + "sync" "time" ) @@ -29,6 +30,7 @@ type Client struct { SecretKey string HTTPClient *http.Client token *TokenCache + tokenMutex sync.RWMutex } // TokenCache stores the JWT token and its expiration @@ -66,17 +68,31 @@ func NewClient(orgID, secretKey string) *Client { // GetToken retrieves a valid JWT token (from cache or by requesting a new one) func (c *Client) GetToken() (string, error) { - // Check if we have a valid cached token - if c.token != nil && time.Now().Before(c.token.ExpiresAt.Add(-1*time.Minute)) { + // Check if we have a valid cached token with read lock + c.tokenMutex.RLock() + if c.token != nil && time.Now().Before(c.token.ExpiresAt.Add(-3*time.Minute)) { + token := c.token.Token + c.tokenMutex.RUnlock() + return token, nil + } + c.tokenMutex.RUnlock() + + // Request a new token with write lock + c.tokenMutex.Lock() + defer c.tokenMutex.Unlock() + + // Double-check after acquiring write lock (another goroutine may have refreshed) + if c.token != nil && time.Now().Before(c.token.ExpiresAt.Add(-3*time.Minute)) { return c.token.Token, nil } // Request a new token - return c.requestNewToken() + return c.requestNewTokenLocked() } -// requestNewToken requests a new JWT token using AWS SigV4 authentication -func (c *Client) requestNewToken() (string, error) { +// requestNewTokenLocked requests a new JWT token using AWS SigV4 authentication +// Caller must hold tokenMutex write lock +func (c *Client) requestNewTokenLocked() (string, error) { path := "/auth/token" url := c.BaseURL + path @@ -143,10 +159,12 @@ func (c *Client) signRequest(req *http.Request, path, body string) error { // Create canonical request canonicalHeaders := fmt.Sprintf("x-amz-date:%s\n", amzDate) signedHeaders := "x-amz-date" + canonicalQueryString := "" // Empty for auth endpoint, can be extended for other endpoints - canonicalRequest := fmt.Sprintf("%s\n%s\n\n%s\n%s\n%s", + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", req.Method, path, + canonicalQueryString, canonicalHeaders, signedHeaders, payloadHash, From 2d3dc7d23b72b698ff1a8d658e0d9a18eece0861 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:16:21 +0000 Subject: [PATCH 4/4] fix: improve examples and documentation security - Update secret key placeholder in config example - Fix troubleshooting section to not print secrets - Add error handling for command substitution in CI script - Add checksum verification for binary download in GitHub Action - Fix GitHub Actions conditional expression syntax - Add error handling after jq commands Co-authored-by: 0x73746F66 <93355168+0x73746F66@users.noreply.github.com> --- docs/VDB-QUICKSTART.md | 26 +++++++++++++++++++++----- examples/vdb-ci-example.sh | 4 +--- examples/vdb-config.json | 2 +- examples/vdb-github-action.yml | 22 +++++++++++++++++----- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/docs/VDB-QUICKSTART.md b/docs/VDB-QUICKSTART.md index 0a51a53..5805559 100644 --- a/docs/VDB-QUICKSTART.md +++ b/docs/VDB-QUICKSTART.md @@ -162,12 +162,28 @@ done < cve-list.txt **Solution**: ```bash -# Verify environment variables are set -echo $VVD_ORG -echo $VVD_SECRET +# Verify environment variables are set WITHOUT printing secret values +if [ -n "${VVD_ORG:-}" ]; then + echo "VVD_ORG is set" +else + echo "VVD_ORG is NOT set" +fi + +if [ -n "${VVD_SECRET:-}" ]; then + echo "VVD_SECRET is set" +else + echo "VVD_SECRET is NOT set" +fi + +# Check that the config file exists (but don't print its contents) +if [ -f "$HOME/.vulnetix/vdb.json" ]; then + echo "VDB config file found at $HOME/.vulnetix/vdb.json" +else + echo "VDB config file not found at $HOME/.vulnetix/vdb.json" +fi -# Or check config file exists -cat ~/.vulnetix/vdb.json +# Security tip: avoid running commands that print secrets (UUIDs, API keys, +# or full config files) directly to your terminal or CI logs. ``` ### "Invalid signature" Error diff --git a/examples/vdb-ci-example.sh b/examples/vdb-ci-example.sh index 423d065..64134f2 100644 --- a/examples/vdb-ci-example.sh +++ b/examples/vdb-ci-example.sh @@ -46,9 +46,7 @@ echo "📊 Severity threshold: $SEVERITY_THRESHOLD" echo "" # Fetch vulnerabilities -VULNS_JSON=$(vulnetix vdb vulns "$PACKAGE_NAME" -o json 2>&1) - -if [ $? -ne 0 ]; then +if ! VULNS_JSON=$(vulnetix vdb vulns "$PACKAGE_NAME" -o json 2>&1); then echo -e "${RED}❌ Failed to fetch vulnerabilities${NC}" echo "$VULNS_JSON" exit 2 diff --git a/examples/vdb-config.json b/examples/vdb-config.json index 25d0d36..4f8ee7c 100644 --- a/examples/vdb-config.json +++ b/examples/vdb-config.json @@ -1,4 +1,4 @@ { "org_id": "00000000-0000-0000-0000-000000000000", - "secret_key": "0000000000000000000000000000000000000000000000000000000000000000" + "secret_key": "REPLACE_WITH_YOUR_SECRET_KEY" } diff --git a/examples/vdb-github-action.yml b/examples/vdb-github-action.yml index 88aa235..4906928 100644 --- a/examples/vdb-github-action.yml +++ b/examples/vdb-github-action.yml @@ -43,6 +43,9 @@ jobs: run: | # Install from GitHub releases (adjust version as needed) curl -LO https://github.com/vulnetix/cli/releases/latest/download/vulnetix-linux-amd64 + curl -LO https://github.com/vulnetix/cli/releases/latest/download/vulnetix-linux-amd64.sha256 + # Verify the downloaded binary against its SHA-256 checksum + sha256sum -c vulnetix-linux-amd64.sha256 chmod +x vulnetix-linux-amd64 sudo mv vulnetix-linux-amd64 /usr/local/bin/vulnetix vulnetix version @@ -69,10 +72,19 @@ jobs: # Fetch vulnerabilities vulnetix vdb vulns "${{ matrix.package }}" -o json > vulns.json - # Parse results - TOTAL=$(jq '.total' vulns.json) - CRITICAL=$(jq '[.vulnerabilities[] | select(.severity == "CRITICAL")] | length' vulns.json) - HIGH=$(jq '[.vulnerabilities[] | select(.severity == "HIGH")] | length' vulns.json) + # Parse results with error handling + if ! TOTAL=$(jq -r '.total' vulns.json); then + echo "Error: Failed to parse total from JSON" + exit 1 + fi + if ! CRITICAL=$(jq '[.vulnerabilities[] | select(.severity == "CRITICAL")] | length' vulns.json); then + echo "Error: Failed to parse critical count from JSON" + exit 1 + fi + if ! HIGH=$(jq '[.vulnerabilities[] | select(.severity == "HIGH")] | length' vulns.json); then + echo "Error: Failed to parse high count from JSON" + exit 1 + fi echo "total=$TOTAL" >> $GITHUB_OUTPUT echo "critical=$CRITICAL" >> $GITHUB_OUTPUT @@ -91,7 +103,7 @@ jobs: retention-days: 30 - name: Check vulnerability threshold - if: steps.scan.outputs.critical > 0 || steps.scan.outputs.high > 0 + if: steps.scan.outputs.critical != '0' || steps.scan.outputs.high != '0' run: | echo "❌ Critical or High severity vulnerabilities found!" echo "Critical: ${{ steps.scan.outputs.critical }}"