jq for Security Scanning

Security data comes as JSON. Master these patterns to audit vulnerabilities across your repositories at scale.


GitHub Dependabot Alerts

GitHub’s Dependabot scans package-lock.json, requirements.txt, go.sum, and other dependency files for known vulnerabilities.

Basic Alert Listing

# List all alerts for a repo
gh api repos/{owner}/{repo}/dependabot/alerts | jq '.[] | {
  package: .dependency.package.name,
  severity: .security_advisory.severity,
  summary: .security_advisory.summary
}'
Example Output
{
  "package": "minimatch",
  "severity": "high",
  "summary": "minimatch has ReDoS vulnerability"
}

Filter by Severity

# Critical and high only
gh api repos/{owner}/{repo}/dependabot/alerts | jq '
  .[] | select(.security_advisory.severity == "critical" or .security_advisory.severity == "high") |
  {package: .dependency.package.name, severity: .security_advisory.severity, file: .dependency.manifest_path}
'

# High severity with file location
gh api repos/{owner}/{repo}/dependabot/alerts | jq '
  .[] | select(.security_advisory.severity == "high") |
  {package: .dependency.package.name, file: .dependency.manifest_path, summary: .security_advisory.summary}
'

Count by Severity

# Severity distribution
gh api repos/{owner}/{repo}/dependabot/alerts | jq '
  group_by(.security_advisory.severity) |
  map({severity: .[0].security_advisory.severity, count: length}) |
  sort_by(.count) | reverse
'
Example Output
[
  {"severity": "high", "count": 8},
  {"severity": "moderate", "count": 3},
  {"severity": "low", "count": 1}
]

Alerts by Manifest File

Find which package.json or requirements.txt is causing the most alerts:

gh api repos/{owner}/{repo}/dependabot/alerts | jq '
  group_by(.dependency.manifest_path) |
  map({
    file: .[0].dependency.manifest_path,
    count: length,
    packages: [.[] | .dependency.package.name] | unique,
    severities: [.[] | .security_advisory.severity] | group_by(.) | map({(.[0]): length}) | add
  }) |
  sort_by(.count) | reverse
'
Example Output
[
  {
    "file": "02_Assets/old-project/package-lock.json",
    "count": 5,
    "packages": ["minimatch", "glob-parent", "ansi-regex"],
    "severities": {"high": 3, "moderate": 2}
  }
]

Open Alerts with CVE IDs

# CVE details for open alerts
gh api repos/{owner}/{repo}/dependabot/alerts | jq '
  .[] | select(.state == "open") |
  {
    cve: .security_advisory.cve_id,
    ghsa: .security_advisory.ghsa_id,
    package: .dependency.package.name,
    severity: .security_advisory.severity,
    published: .security_advisory.published_at | split("T")[0]
  }
'

Table Output for Reports

# Tab-separated for spreadsheet/column
gh api repos/{owner}/{repo}/dependabot/alerts | jq -r '
  ["PACKAGE", "SEVERITY", "CVE", "FILE"],
  (.[] | [
    .dependency.package.name,
    .security_advisory.severity,
    (.security_advisory.cve_id // "N/A"),
    .dependency.manifest_path
  ]) | @tsv
' | column -t
Example Output
PACKAGE      SEVERITY  CVE             FILE
minimatch    high      CVE-2022-3517   package-lock.json
glob-parent  high      CVE-2020-28469  package-lock.json
ansi-regex   moderate  CVE-2021-3807   package-lock.json

Multi-Repo Scanning

Scan all your repositories for vulnerabilities:

# Get all repos, check each for alerts
gh repo list --limit 100 --json name -q '.[].name' | while read repo; do
  COUNT=$(gh api "repos/{owner}/$repo/dependabot/alerts" 2>/dev/null | jq 'length')
  if [[ "$COUNT" -gt 0 ]]; then
    echo "$repo: $COUNT alerts"
  fi
done

# Detailed multi-repo report
gh repo list --limit 100 --json name -q '.[].name' | while read repo; do
  gh api "repos/{owner}/$repo/dependabot/alerts" 2>/dev/null | jq --arg repo "$repo" '
    .[] | select(.security_advisory.severity == "critical" or .security_advisory.severity == "high") |
    {repo: $repo, package: .dependency.package.name, severity: .security_advisory.severity}
  '
done | jq -s '.'

GitLab Vulnerability Reports

GitLab’s Security Dashboard provides vulnerability findings from SAST, DAST, dependency scanning, and container scanning.

Project Vulnerability Findings

# Get project ID first
PROJECT_ID=$(glab api projects --jq '.[] | select(.path == "your-project") | .id')

# List all vulnerabilities
glab api "projects/$PROJECT_ID/vulnerability_findings" | jq '
  .[] | {
    severity: .severity,
    name: .name,
    scanner: .scanner.name,
    state: .state
  }
'

Filter Critical/High

glab api "projects/$PROJECT_ID/vulnerability_findings" | jq '
  .[] | select(.severity == "critical" or .severity == "high") |
  {
    name: .name,
    severity: .severity,
    state: .state,
    scanner: .scanner.name,
    location: .location.file
  }
'

Count by Scanner Type

glab api "projects/$PROJECT_ID/vulnerability_findings" | jq '
  group_by(.scanner.name) |
  map({scanner: .[0].scanner.name, count: length, critical: [.[] | select(.severity == "critical")] | length}) |
  sort_by(.count) | reverse
'
Example Output
[
  {"scanner": "dependency_scanning", "count": 15, "critical": 2},
  {"scanner": "sast", "count": 8, "critical": 0},
  {"scanner": "container_scanning", "count": 3, "critical": 1}
]

Dependency Scanning Details

glab api "projects/$PROJECT_ID/vulnerability_findings" | jq '
  .[] | select(.scanner.name == "dependency_scanning") |
  {
    package: .location.dependency.package.name,
    version: .location.dependency.version,
    severity: .severity,
    solution: .solution
  }
'

Group Vulnerabilities

# Get vulnerability groups with counts
glab api "groups/:group_id/vulnerability_findings_summary" | jq '
  {
    critical: .counts.critical,
    high: .counts.high,
    medium: .counts.medium,
    low: .counts.low,
    total: (.counts.critical + .counts.high + .counts.medium + .counts.low)
  }
'

Common Security Audit Patterns

Compare Before/After Scans

# Save baseline
gh api repos/{owner}/{repo}/dependabot/alerts > baseline.json

# After remediation, compare
gh api repos/{owner}/{repo}/dependabot/alerts > current.json

# Find resolved (in baseline but not current)
jq -n --slurpfile old baseline.json --slurpfile new current.json '
  ($old[0] | map(.number)) - ($new[0] | map(.number)) |
  . as $resolved |
  $old[0] | map(select(.number | IN($resolved[]))) |
  map({number, package: .dependency.package.name, severity: .security_advisory.severity})
'

# Find new (in current but not baseline)
jq -n --slurpfile old baseline.json --slurpfile new current.json '
  ($new[0] | map(.number)) - ($old[0] | map(.number)) |
  . as $new_alerts |
  $new[0] | map(select(.number | IN($new_alerts[]))) |
  map({number, package: .dependency.package.name, severity: .security_advisory.severity})
'

Age Analysis

Find vulnerabilities that have been open for too long:

# Alerts open > 30 days
gh api repos/{owner}/{repo}/dependabot/alerts | jq '
  (now | floor) as $now |
  .[] | select(.state == "open") |
  (.created_at | fromdateiso8601 | floor) as $created |
  (($now - $created) / 86400 | floor) as $age_days |
  select($age_days > 30) |
  {
    package: .dependency.package.name,
    severity: .security_advisory.severity,
    age_days: $age_days,
    created: .created_at | split("T")[0]
  }
' | jq -s 'sort_by(.age_days) | reverse'

CVSS Score Extraction

# Sort by CVSS score (highest risk first)
gh api repos/{owner}/{repo}/dependabot/alerts | jq '
  [.[] | {
    package: .dependency.package.name,
    cvss: .security_advisory.cvss.score,
    severity: .security_advisory.severity,
    vector: .security_advisory.cvss.vector_string
  }] | sort_by(.cvss) | reverse
'

Remediation Priority List

Combine severity, age, and CVSS for a prioritized fix list:

gh api repos/{owner}/{repo}/dependabot/alerts | jq '
  (now | floor) as $now |
  [.[] | select(.state == "open") |
    (.created_at | fromdateiso8601 | floor) as $created |
    (($now - $created) / 86400 | floor) as $age |
    {
      package: .dependency.package.name,
      file: .dependency.manifest_path,
      severity: .security_advisory.severity,
      cvss: (.security_advisory.cvss.score // 0),
      age_days: $age,
      priority_score: (
        (if .security_advisory.severity == "critical" then 100
         elif .security_advisory.severity == "high" then 75
         elif .security_advisory.severity == "medium" then 50
         else 25 end) +
        ((.security_advisory.cvss.score // 0) * 5) +
        (if $age > 90 then 20 elif $age > 30 then 10 else 0 end)
      )
    }
  ] | sort_by(.priority_score) | reverse | .[0:10]
'

Automation Scripts

Weekly Security Digest

#!/bin/bash
# security-digest.sh - Weekly vulnerability summary

OWNER="EvanusModestus"
REPOS=$(gh repo list --limit 100 --json name -q '.[].name')

echo "# Security Digest - $(date +%Y-%m-%d)"
echo ""

for repo in $REPOS; do
  ALERTS=$(gh api "repos/$OWNER/$repo/dependabot/alerts" 2>/dev/null)

  if [[ $(echo "$ALERTS" | jq 'length') -gt 0 ]]; then
    echo "## $repo"
    echo "$ALERTS" | jq -r '
      group_by(.security_advisory.severity) |
      map("\(.[0].security_advisory.severity): \(length)") |
      join(", ")
    '
    echo ""
  fi
done

Slack/Webhook Alert

#!/bin/bash
# alert-critical.sh - Alert on new critical vulnerabilities

CRITICAL=$(gh api repos/{owner}/{repo}/dependabot/alerts | jq '
  [.[] | select(.state == "open" and .security_advisory.severity == "critical")]
')

COUNT=$(echo "$CRITICAL" | jq 'length')

if [[ "$COUNT" -gt 0 ]]; then
  MESSAGE=$(echo "$CRITICAL" | jq -r '
    "Critical vulnerabilities found:\n" +
    ([.[] | "• \(.dependency.package.name): \(.security_advisory.summary)"] | join("\n"))
  ')

  curl -X POST "$SLACK_WEBHOOK" \
    -H 'Content-type: application/json' \
    -d "{\"text\": \"$MESSAGE\"}"
fi