Skip to content

Commit 0827a32

Browse files
Piyush Singh GaurPiyush Singh Gaur
authored andcommitted
feat(deps): automated Remediation Strategy for Trivy-Detected NPM Vulnerabilities
Automated Remediation Strategy for Trivy-Detected NPM Vulnerabilities
1 parent b8ad262 commit 0827a32

File tree

9 files changed

+345
-1
lines changed

9 files changed

+345
-1
lines changed

.eslintignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
node_modules/
22
dist/
33
coverage/
4-
4+
scripts/
55
.eslintrc.js
66
index.*
77
commitlint.config.js

.eslintrc.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,18 @@ module.exports = {
1515
project: './tsconfig.json',
1616
tsconfigRootDir: __dirname,
1717
},
18+
overrides: [
19+
{
20+
// scripts/ are plain Node.js JS files not covered by tsconfig.json,
21+
// so disable typed linting rules for them
22+
files: ['scripts/**/*.js'],
23+
parserOptions: {
24+
project: null,
25+
},
26+
rules: {
27+
'@typescript-eslint/no-var-requires': 'off',
28+
'@typescript-eslint/no-misused-promises': 'off',
29+
},
30+
},
31+
],
1832
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Trivy Security Remediation
2+
3+
on:
4+
schedule:
5+
- cron: '0 3 * * *'
6+
pull_request:
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
pull-requests: write
12+
13+
jobs:
14+
security-remediation:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
with:
21+
ref: ${{ github.ref }}
22+
fetch-depth: 0
23+
24+
- name: Setup Node
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version: 22
28+
29+
- name: Install dependencies
30+
run: npm ci --ignore-scripts
31+
32+
- name: Install Trivy
33+
uses: aquasecurity/setup-trivy@v0.2.0
34+
35+
- name: Check Trivy version
36+
run: trivy --version
37+
38+
- name: Run remediation workflow
39+
run: bash scripts/trivy-remediation.sh
40+
41+
- name: Debug git changes
42+
run: |
43+
git status
44+
git diff
45+
46+
- name: Stage dependency changes
47+
run: |
48+
git add package.json package-lock.json || true
49+
50+
- name: Check for changes
51+
id: changes
52+
run: |
53+
if ! git diff --cached --quiet; then
54+
echo "changed=true" >> $GITHUB_OUTPUT
55+
else
56+
echo "changed=false" >> $GITHUB_OUTPUT
57+
fi
58+
59+
- name: Create Pull Request
60+
if: steps.changes.outputs.changed == 'true'
61+
uses: peter-evans/create-pull-request@v6
62+
with:
63+
branch: security/trivy-remediation
64+
base: feat/automation
65+
add-paths: |
66+
package.json
67+
package-lock.json
68+
commit-message: 'fix(security): automated Trivy remediation'
69+
title: Automated Trivy vulnerability remediation
70+
body: Automated fix for HIGH and CRITICAL vulnerabilities detected by Trivy.

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"jsdom": "^21.0.0",
9090
"nyc": "^17.1.0",
9191
"semantic-release": "^25.0.1",
92+
"semver": "^7.5.4",
9293
"simple-git": "^3.15.1",
9394
"source-map-support": "^0.5.21",
9495
"typescript": "~5.2.2"

scripts/audit-remediation.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
echo "Running npm audit fix"
6+
7+
npm audit fix --ignore-scripts || true
8+
9+
echo "Updating lockfile"
10+
11+
npm i --package-lock-only --ignore-scripts
12+
13+
echo "Installing dependencies"
14+
15+
npm ci --ignore-scripts

scripts/dependency-fix.js

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
const fs = require('fs');
2+
const {execSync} = require('child_process');
3+
const semver = require('semver');
4+
5+
console.log('Starting Trivy dependency remediation');
6+
7+
const report = JSON.parse(fs.readFileSync('trivy-report.json', 'utf8'));
8+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
9+
10+
const processed = new Set();
11+
const newOverrides = {};
12+
13+
let deps = {
14+
...pkg.dependencies,
15+
...pkg.devDependencies,
16+
};
17+
18+
let dependencyTree = {};
19+
20+
function buildDependencyTree() {
21+
try {
22+
const tree = execSync('npm ls --json', {encoding: 'utf8'});
23+
dependencyTree = JSON.parse(tree);
24+
} catch (e) {
25+
dependencyTree = {};
26+
}
27+
}
28+
29+
function refreshDependencies() {
30+
const updated = JSON.parse(fs.readFileSync('package.json', 'utf8'));
31+
32+
deps = {
33+
...updated.dependencies,
34+
...updated.devDependencies,
35+
};
36+
37+
buildDependencyTree();
38+
}
39+
40+
function isDirect(name) {
41+
return deps[name] !== undefined;
42+
}
43+
44+
function cleanVersion(v) {
45+
if (!v) return null;
46+
return v.replace('^', '').replace('~', '');
47+
}
48+
49+
function safeUpgrade(current, target) {
50+
const c = semver.coerce(current);
51+
const t = semver.coerce(target);
52+
53+
if (!c || !t) return false;
54+
55+
const diff = semver.diff(c, t);
56+
57+
return diff === 'patch' || diff === 'minor';
58+
}
59+
60+
function findInstalledVersion(pkgName) {
61+
function search(node) {
62+
if (!node.dependencies) return null;
63+
64+
for (const [name, data] of Object.entries(node.dependencies)) {
65+
if (name === pkgName) return data.version;
66+
67+
const nested = search(data);
68+
69+
if (nested) return nested;
70+
}
71+
72+
return null;
73+
}
74+
75+
return search(dependencyTree);
76+
}
77+
78+
function findParentDependency(pkgName) {
79+
function search(node) {
80+
if (!node.dependencies) return null;
81+
82+
for (const [name, data] of Object.entries(node.dependencies)) {
83+
if (data.dependencies && data.dependencies[pkgName]) {
84+
return name;
85+
}
86+
87+
const nested = search(data);
88+
89+
if (nested) return nested;
90+
}
91+
92+
return null;
93+
}
94+
95+
return search(dependencyTree);
96+
}
97+
98+
buildDependencyTree();
99+
100+
for (const result of report.Results || []) {
101+
if (result.Type !== 'npm') continue;
102+
103+
for (const vuln of result.Vulnerabilities || []) {
104+
const name = vuln.PkgName;
105+
const fixed = vuln.FixedVersion?.split(',')[0]?.trim();
106+
107+
if (!name || !fixed) continue;
108+
109+
if (processed.has(name)) continue;
110+
processed.add(name);
111+
112+
console.log(`Processing vulnerability: ${name}`);
113+
114+
const installed = findInstalledVersion(name);
115+
116+
if (installed && semver.gte(installed, fixed)) {
117+
console.log(`${name} already resolved to safe version ${installed}`);
118+
continue;
119+
}
120+
121+
const current = isDirect(name) ? cleanVersion(deps[name]) : null;
122+
123+
if (isDirect(name)) {
124+
console.log(`Direct dependency detected: ${name}`);
125+
126+
if (!safeUpgrade(current, fixed)) {
127+
console.warn(
128+
`Skipping unsafe upgrade ${name} (${current} -> ${fixed})`,
129+
);
130+
} else {
131+
try {
132+
console.log(`Updating direct dependency ${name} -> ${fixed}`);
133+
134+
execSync(`npm i ${name}@${fixed} --ignore-scripts`, {
135+
stdio: 'inherit',
136+
});
137+
138+
refreshDependencies();
139+
140+
const updatedVersion = findInstalledVersion(name);
141+
142+
if (updatedVersion && semver.gte(updatedVersion, fixed)) {
143+
console.log(`${name} resolved via direct upgrade`);
144+
continue;
145+
}
146+
} catch {
147+
console.log(`Direct upgrade failed for ${name}`);
148+
}
149+
}
150+
}
151+
152+
const parent = findParentDependency(name);
153+
154+
if (parent && deps[parent]) {
155+
console.log(`Attempting parent dependency upgrade: ${parent}`);
156+
157+
try {
158+
const parentVersion = cleanVersion(deps[parent]);
159+
160+
if (parentVersion) {
161+
execSync(`npm i ${parent}@^${parentVersion} --ignore-scripts`, {
162+
stdio: 'inherit',
163+
});
164+
165+
refreshDependencies();
166+
167+
const updatedVersion = findInstalledVersion(name);
168+
169+
if (updatedVersion && semver.gte(updatedVersion, fixed)) {
170+
console.log(`${name} resolved via parent upgrade (${parent})`);
171+
continue;
172+
}
173+
}
174+
} catch {
175+
console.log(`Parent upgrade failed for ${parent}`);
176+
}
177+
}
178+
179+
if (!pkg.overrides || !pkg.overrides[name]) {
180+
console.log(`Adding override for ${name}`);
181+
182+
newOverrides[name] = `^${fixed}`;
183+
}
184+
}
185+
}
186+
187+
const updatedPkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
188+
189+
if (!updatedPkg.overrides) updatedPkg.overrides = {};
190+
191+
Object.assign(updatedPkg.overrides, newOverrides);
192+
193+
fs.writeFileSync('package.json', JSON.stringify(updatedPkg, null, 2) + '\n');
194+
195+
if (Object.keys(newOverrides).length > 0) {
196+
console.log('Updating lockfile for overrides');
197+
198+
execSync('npm i --package-lock-only --ignore-scripts', {stdio: 'inherit'});
199+
}
200+
201+
console.log('Trivy dependency remediation completed');

scripts/trivy-remediation.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
echo "Step 1: Run Trivy scan"
6+
bash scripts/trivy-scan.sh
7+
8+
echo "Step 2: Run npm audit fix"
9+
bash scripts/audit-remediation.sh
10+
11+
echo "Step 3: Re-scan vulnerabilities"
12+
13+
trivy fs \
14+
--severity HIGH,CRITICAL \
15+
--format json \
16+
-o trivy-after.json \
17+
.
18+
19+
mv trivy-after.json trivy-report.json
20+
21+
echo "Step 4: Fix remaining vulnerabilities"
22+
23+
node scripts/dependency-fix.js
24+
25+
echo "Step 5: Update lockfile"
26+
27+
npm i --package-lock-only --ignore-scripts
28+
npm ci --ignore-scripts

scripts/trivy-scan.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
echo "Running Trivy scan..."
6+
7+
trivy fs \
8+
--scanners vuln \
9+
--severity HIGH,CRITICAL \
10+
--format json \
11+
-o trivy-report.json \
12+
.
13+
14+
echo "Trivy scan completed"

0 commit comments

Comments
 (0)