From ae69645df17f6357de419a3b65353e3b103ec899 Mon Sep 17 00:00:00 2001 From: Piyush Singh Gaur Date: Tue, 10 Mar 2026 14:40:47 +0530 Subject: [PATCH] feat(deps): automated Remediation Strategy for Trivy-Detected NPM Vulnerabilities Automated Remediation Strategy for Trivy-Detected NPM Vulnerabilities --- .eslintignore | 2 +- .eslintrc.js | 14 ++ .github/workflows/trivy-remediation.yaml | 74 +++++++++ package-lock.json | 1 + package.json | 1 + scripts/audit-remediation.sh | 15 ++ scripts/dependency-fix.js | 201 +++++++++++++++++++++++ scripts/trivy-remediation.sh | 28 ++++ scripts/trivy-scan.sh | 14 ++ 9 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/trivy-remediation.yaml create mode 100644 scripts/audit-remediation.sh create mode 100644 scripts/dependency-fix.js create mode 100644 scripts/trivy-remediation.sh create mode 100644 scripts/trivy-scan.sh diff --git a/.eslintignore b/.eslintignore index e9fc1a7..00b400c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,7 +1,7 @@ node_modules/ dist/ coverage/ - +scripts/ .eslintrc.js index.* commitlint.config.js diff --git a/.eslintrc.js b/.eslintrc.js index 3be81c7..4697856 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,4 +15,18 @@ module.exports = { project: './tsconfig.json', tsconfigRootDir: __dirname, }, + overrides: [ + { + // scripts/ are plain Node.js JS files not covered by tsconfig.json, + // so disable typed linting rules for them + files: ['scripts/**/*.js'], + parserOptions: { + project: null, + }, + rules: { + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-misused-promises': 'off', + }, + }, + ], }; diff --git a/.github/workflows/trivy-remediation.yaml b/.github/workflows/trivy-remediation.yaml new file mode 100644 index 0000000..aeb7c22 --- /dev/null +++ b/.github/workflows/trivy-remediation.yaml @@ -0,0 +1,74 @@ +name: Trivy Security Remediation + +on: + schedule: + - cron: '0 3 * * *' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +env: + CONFIG_USERNAME: ${{ vars.GIT_COMMIT_USERNAME }} + CONFIG_EMAIL: ${{ vars.GIT_COMMIT_EMAIL }} + +jobs: + security-remediation: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: master + fetch-depth: 0 + persist-credentials: true + token: ${{ secrets.RELEASE_COMMIT_GH_PAT }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Configure Git + run: | + git config --global user.name "$CONFIG_USERNAME" + git config --global user.email "$CONFIG_EMAIL" + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Install Trivy + uses: aquasecurity/setup-trivy@v0.2.0 + + - name: Run remediation workflow + run: bash scripts/trivy-remediation.sh + + - name: Stage dependency changes + run: | + git add package.json package-lock.json || true + + - name: Check for changes + id: changes + run: | + if ! git diff --cached --quiet; then + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "changed=false" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.changes.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.RELEASE_COMMIT_GH_PAT }} + branch: security/trivy-remediation + base: master + delete-branch: true + add-paths: | + package.json + package-lock.json + commit-message: 'fix(security): automated Trivy remediation' + title: 'fix(security): Automated Trivy vulnerability remediation' + body: Automated fix for HIGH and CRITICAL vulnerabilities detected by Trivy. diff --git a/package-lock.json b/package-lock.json index bf5a6db..f08836d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "jsdom": "^21.0.0", "nyc": "^17.1.0", "semantic-release": "^25.0.1", + "semver": "^7.5.4", "simple-git": "^3.15.1", "source-map-support": "^0.5.21", "typescript": "~5.2.2" diff --git a/package.json b/package.json index 1aed68a..ed633ba 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "jsdom": "^21.0.0", "nyc": "^17.1.0", "semantic-release": "^25.0.1", + "semver": "^7.5.4", "simple-git": "^3.15.1", "source-map-support": "^0.5.21", "typescript": "~5.2.2" diff --git a/scripts/audit-remediation.sh b/scripts/audit-remediation.sh new file mode 100644 index 0000000..d889025 --- /dev/null +++ b/scripts/audit-remediation.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +echo "Running npm audit fix" + +npm audit fix --ignore-scripts || true + +echo "Updating lockfile" + +npm i --package-lock-only --ignore-scripts + +echo "Installing dependencies" + +npm ci --ignore-scripts diff --git a/scripts/dependency-fix.js b/scripts/dependency-fix.js new file mode 100644 index 0000000..7cea3fc --- /dev/null +++ b/scripts/dependency-fix.js @@ -0,0 +1,201 @@ +const fs = require('fs'); +const {execSync} = require('child_process'); +const semver = require('semver'); + +console.log('Starting Trivy dependency remediation'); + +const report = JSON.parse(fs.readFileSync('trivy-report.json', 'utf8')); +const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + +const processed = new Set(); +const newOverrides = {}; + +let deps = { + ...pkg.dependencies, + ...pkg.devDependencies, +}; + +let dependencyTree = {}; + +function buildDependencyTree() { + try { + const tree = execSync('npm ls --json', {encoding: 'utf8'}); + dependencyTree = JSON.parse(tree); + } catch (e) { + dependencyTree = {}; + } +} + +function refreshDependencies() { + const updated = JSON.parse(fs.readFileSync('package.json', 'utf8')); + + deps = { + ...updated.dependencies, + ...updated.devDependencies, + }; + + buildDependencyTree(); +} + +function isDirect(name) { + return deps[name] !== undefined; +} + +function cleanVersion(v) { + if (!v) return null; + return v.replace('^', '').replace('~', ''); +} + +function safeUpgrade(current, target) { + const c = semver.coerce(current); + const t = semver.coerce(target); + + if (!c || !t) return false; + + const diff = semver.diff(c, t); + + return diff === 'patch' || diff === 'minor'; +} + +function findInstalledVersion(pkgName) { + function search(node) { + if (!node.dependencies) return null; + + for (const [name, data] of Object.entries(node.dependencies)) { + if (name === pkgName) return data.version; + + const nested = search(data); + + if (nested) return nested; + } + + return null; + } + + return search(dependencyTree); +} + +function findParentDependency(pkgName) { + function search(node) { + if (!node.dependencies) return null; + + for (const [name, data] of Object.entries(node.dependencies)) { + if (data.dependencies && data.dependencies[pkgName]) { + return name; + } + + const nested = search(data); + + if (nested) return nested; + } + + return null; + } + + return search(dependencyTree); +} + +buildDependencyTree(); + +for (const result of report.Results || []) { + if (result.Type !== 'npm') continue; + + for (const vuln of result.Vulnerabilities || []) { + const name = vuln.PkgName; + const fixed = vuln.FixedVersion?.split(',')[0]?.trim(); + + if (!name || !fixed) continue; + + if (processed.has(name)) continue; + processed.add(name); + + console.log(`Processing vulnerability: ${name}`); + + const installed = findInstalledVersion(name); + + if (installed && semver.gte(installed, fixed)) { + console.log(`${name} already resolved to safe version ${installed}`); + continue; + } + + const current = isDirect(name) ? cleanVersion(deps[name]) : null; + + if (isDirect(name)) { + console.log(`Direct dependency detected: ${name}`); + + if (!safeUpgrade(current, fixed)) { + console.warn( + `Skipping unsafe upgrade ${name} (${current} -> ${fixed})`, + ); + } else { + try { + console.log(`Updating direct dependency ${name} -> ${fixed}`); + + execSync(`npm i ${name}@${fixed} --ignore-scripts`, { + stdio: 'inherit', + }); + + refreshDependencies(); + + const updatedVersion = findInstalledVersion(name); + + if (updatedVersion && semver.gte(updatedVersion, fixed)) { + console.log(`${name} resolved via direct upgrade`); + continue; + } + } catch { + console.log(`Direct upgrade failed for ${name}`); + } + } + } + + const parent = findParentDependency(name); + + if (parent && deps[parent]) { + console.log(`Attempting parent dependency upgrade: ${parent}`); + + try { + const parentVersion = cleanVersion(deps[parent]); + + if (parentVersion) { + execSync(`npm i ${parent}@^${parentVersion} --ignore-scripts`, { + stdio: 'inherit', + }); + + refreshDependencies(); + + const updatedVersion = findInstalledVersion(name); + + if (updatedVersion && semver.gte(updatedVersion, fixed)) { + console.log(`${name} resolved via parent upgrade (${parent})`); + continue; + } + } + } catch { + console.log(`Parent upgrade failed for ${parent}`); + } + } + + if (!pkg.overrides || !pkg.overrides[name]) { + console.log(`Adding override for ${name}`); + + newOverrides[name] = `^${fixed}`; + } + } +} + +const updatedPkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + +if (!updatedPkg.overrides) updatedPkg.overrides = {}; + +Object.assign(updatedPkg.overrides, newOverrides); + +fs.writeFileSync('package.json', JSON.stringify(updatedPkg, null, 2) + '\n'); + +if (Object.keys(newOverrides).length > 0) { + console.log('Updating lockfile for overrides'); + + execSync('npm i --package-lock-only --ignore-scripts', {stdio: 'inherit'}); +} + +console.log('Trivy dependency remediation completed'); diff --git a/scripts/trivy-remediation.sh b/scripts/trivy-remediation.sh new file mode 100644 index 0000000..fb30b24 --- /dev/null +++ b/scripts/trivy-remediation.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -e + +echo "Step 1: Run Trivy scan" +bash scripts/trivy-scan.sh + +echo "Step 2: Run npm audit fix" +bash scripts/audit-remediation.sh + +echo "Step 3: Re-scan vulnerabilities" + +trivy fs \ +--severity HIGH,CRITICAL \ +--format json \ +-o trivy-after.json \ +. + +mv trivy-after.json trivy-report.json + +echo "Step 4: Fix remaining vulnerabilities" + +node scripts/dependency-fix.js + +echo "Step 5: Update lockfile" + +npm i --package-lock-only --ignore-scripts +npm ci --ignore-scripts diff --git a/scripts/trivy-scan.sh b/scripts/trivy-scan.sh new file mode 100644 index 0000000..94a44e4 --- /dev/null +++ b/scripts/trivy-scan.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +echo "Running Trivy scan..." + +trivy fs \ +--scanners vuln \ +--severity HIGH,CRITICAL \ +--format json \ +-o trivy-report.json \ +. + +echo "Trivy scan completed"