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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 39 additions & 13 deletions cmd/vdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ var (
vdbOrgID string
vdbSecretKey string
vdbBaseURL string
vdbLimit int
vdbOffset int
vdbOutput string
)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)")
}
26 changes: 21 additions & 5 deletions docs/VDB-QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions examples/vdb-ci-example.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/vdb-config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"org_id": "00000000-0000-0000-0000-000000000000",
"secret_key": "0000000000000000000000000000000000000000000000000000000000000000"
"secret_key": "REPLACE_WITH_YOUR_SECRET_KEY"
}
22 changes: 17 additions & 5 deletions examples/vdb-github-action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 }}"
Expand Down
44 changes: 23 additions & 21 deletions internal/vdb/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
30 changes: 24 additions & 6 deletions internal/vdb/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/http"
"os"
"path/filepath"
"sync"
"time"
)

Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down