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/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 }}" 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,