
05-工程化与最佳实践/04-安全加固/02-依赖安全检查
依赖安全检查
学习目标
- 掌握 npm audit 漏洞扫描与修复
- 学会使用 Snyk 进行依赖监控
- 理解依赖更新策略和安全最佳实践
前置知识
- npm/yarn 包管理
- 语义化版本
- CI/CD 基础
知识点列表
1. npm audit 漏洞扫描
1.1 基础使用
# 扫描项目依赖漏洞
npm audit
# 仅显示高严重性漏洞
npm audit –audit-level=high
# 生成 JSON 格式报告
npm audit –json > audit-report.json
# 自动修复可修复的漏洞
npm audit fix
# 强制更新到主要版本
npm audit fix –force
# 只更新生产依赖
npm audit fix –production
# 查看详细信息
npm audit –verbose
// scripts/audit.js
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
class SecurityAudit {
async runAudit(options = {}) {
const cmd = `npm audit –json ${options.production ? '–production' : ''}`;
try {
const { stdout } = await execPromise(cmd);
const report = JSON.parse(stdout);
return this.analyzeReport(report);
} catch (error) {
// npm audit 在有漏洞时会返回非零退出码
if (error.stdout) {
return this.analyzeReport(JSON.parse(error.stdout));
}
throw error;
}
}
analyzeReport(report) {
const summary = {
totalVulnerabilities: report.metadata.vulnerabilities.total,
critical: report.metadata.vulnerabilities.critical || 0,
high: report.metadata.vulnerabilities.high || 0,
moderate: report.metadata.vulnerabilities.moderate || 0,
low: report.metadata.vulnerabilities.low || 0,
affectedPackages: [],
fixable: {
viaInstall: 0,
viaSemver: 0
}
};
for (const [pkg, data] of Object.entries(report.vulnerabilities || {})) {
summary.affectedPackages.push({
package: pkg,
severity: data.severity,
title: data.title,
version: data.via[0]?.version,
fixAvailable: data.fixAvailable !== false
});
if (data.fixAvailable !== false) {
if (typeof data.fixAvailable === 'object') {
summary.fixable.viaSemver++;
} else {
summary.fixable.viaInstall++;
}
}
}
return summary;
}
async generateReport() {
const audit = await this.runAudit();
console.log('\\n=== 安全审计报告 ===');
console.log(`总漏洞数: ${audit.totalVulnerabilities}`);
console.log(`严重: ${audit.critical} | 高危: ${audit.high} | 中危: ${audit.moderate} | 低危: ${audit.low}`);
console.log(`可修复: ${audit.fixable.viaInstall} (自动) + ${audit.fixable.viaSemver} (版本升级)`);
if (audit.affectedPackages.length > 0) {
console.log('\\n受影响包:');
for (const pkg of audit.affectedPackages) {
console.log(` – ${pkg.package}@${pkg.version}`);
console.log(` 严重性: ${pkg.severity}`);
console.log(` 问题: ${pkg.title}`);
console.log(` 可修复: ${pkg.fixAvailable ? '是' : '否'}`);
}
}
return audit;
}
async checkThreshold(threshold = 0) {
const audit = await this.runAudit();
if (audit.totalVulnerabilities > threshold) {
console.error(`❌ 安全审计失败: ${audit.totalVulnerabilities} 个漏洞超过阈值 ${threshold}`);
process.exit(1);
}
console.log(`✅ 安全审计通过: ${audit.totalVulnerabilities} 个漏洞`);
return true;
}
}
// 在 CI 中使用
if (require.main === module) {
const auditor = new SecurityAudit();
auditor.checkThreshold(0).catch(console.error);
}
1.2 自动化审计配置
// package.json
{
"scripts": {
"audit": "npm audit",
"audit:fix": "npm audit fix",
"audit:report": "npm audit –json > audit-report.json",
"security-check": "npm audit –audit-level=high"
},
"husky": {
"hooks": {
"pre-commit": "npm run security-check"
}
}
}
# .github/workflows/security-audit.yml
name: Security Audit
on:
schedule:
– cron: '0 0 * * *' # 每天运行
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
audit:
runs-on: ubuntu–latest
steps:
– uses: actions/checkout@v3
– uses: actions/setup–node@v3
with:
node-version: '18'
cache: 'npm'
– name: Install dependencies
run: npm ci
– name: Run security audit
run: npm audit ––json | tee audit–results.json
– name: Check for critical vulnerabilities
run: |
CRITICAL=$(jq '.metadata.vulnerabilities.critical // 0' audit-results.json)
HIGH=$(jq '.metadata.vulnerabilities.high // 0' audit-results.json)
if [ "$CRITICAL" -gt 0 ] || [ "$HIGH" -gt 0 ]; then
echo "❌ Critical/High vulnerabilities found!"
exit 1
fi
echo "✅ No critical vulnerabilities"
– name: Upload audit report
uses: actions/upload–artifact@v3
if: always()
with:
name: audit–report
path: audit–results.json
2. Snyk 依赖监控
2.1 安装与配置
# 安装 Snyk CLI
npm install -g snyk
# 认证
snyk auth
# 在项目中初始化
snyk monitor
// scripts/snyk-security.js
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
class SnykSecurity {
constructor(projectName) {
this.projectName = projectName;
}
async test() {
console.log('🔍 运行 Snyk 安全测试…');
try {
const { stdout } = await execPromise('snyk test –json');
const result = JSON.parse(stdout);
return this.analyzeResult(result);
} catch (error) {
if (error.stdout) {
return this.analyzeResult(JSON.parse(error.stdout));
}
throw error;
}
}
analyzeResult(result) {
const summary = {
ok: result.ok,
vulnerabilities: {
total: result.vulnerabilities?.length || 0,
critical: 0,
high: 0,
medium: 0,
low: 0
},
licenses: {
total: result.licenses?.length || 0,
issues: []
},
dependencies: result.dependencyCount || 0,
path: result.path
};
// 统计漏洞严重性
for (const vuln of result.vulnerabilities || []) {
summary.vulnerabilities[vuln.severity]++;
summary.vulnerabilities[vuln.severity] =
(summary.vulnerabilities[vuln.severity] || 0) + 1;
}
// 许可证问题
for (const license of result.licenses || []) {
if (license.severity === 'high' || license.issues?.length) {
summary.licenses.issues.push({
package: license.name,
license: license.license,
issues: license.issues
});
}
}
return summary;
}
async monitor() {
console.log('📊 上传项目到 Snyk 监控…');
try {
const { stdout } = await execPromise('snyk monitor –json');
const result = JSON.parse(stdout);
console.log(`✅ 项目已添加到 Snyk: ${result.id}`);
console.log(`📈 查看报告: ${result.uri}`);
return result;
} catch (error) {
console.error('❌ Snyk 监控失败:', error.message);
throw error;
}
}
async testWithFix() {
console.log('🔧 尝试自动修复…');
try {
await execPromise('snyk wizard');
return true;
} catch (error) {
console.error('自动修复失败:', error.message);
return false;
}
}
async generateReport() {
const testResult = await this.test();
console.log('\\n=== Snyk 安全报告 ===');
console.log(`项目: ${this.projectName}`);
console.log(`状态: ${testResult.ok ? '✅ 安全' : '❌ 存在漏洞'}`);
console.log(`依赖总数: ${testResult.dependencies}`);
console.log(`漏洞总数: ${testResult.vulnerabilities.total}`);
if (testResult.vulnerabilities.total > 0) {
console.log('\\n漏洞详情:');
console.log(` 严重: ${testResult.vulnerabilities.critical || 0}`);
console.log(` 高危: ${testResult.vulnerabilities.high || 0}`);
console.log(` 中危: ${testResult.vulnerabilities.medium || 0}`);
console.log(` 低危: ${testResult.vulnerabilities.low || 0}`);
}
if (testResult.licenses.issues.length > 0) {
console.log(`\\n许可证问题: ${testResult.licenses.issues.length}`);
for (const issue of testResult.licenses.issues) {
console.log(` – ${issue.package} (${issue.license})`);
}
}
return testResult;
}
}
// 在 CI 中使用
async function securityCheck() {
const snyk = new SnykSecurity('my-app');
const result = await snyk.test();
if (!result.ok) {
console.error('安全测试失败');
process.exit(1);
}
// 可选:上传到 Snyk 监控
if (process.env.SNYK_MONITOR === 'true') {
await snyk.monitor();
}
}
2.2 Snyk GitHub Actions 集成
# .github/workflows/snyk-security.yml
name: Snyk Security
on:
push:
branches: [ main ]
schedule:
– cron: '0 */6 * * *' # 每6小时
jobs:
security:
runs-on: ubuntu–latest
steps:
– uses: actions/checkout@v3
– name: Setup Node.js
uses: actions/setup–node@v3
with:
node-version: '18'
– name: Install dependencies
run: npm ci
– name: Run Snyk to check for vulnerabilities
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: ––severity–threshold=high
command: test
– name: Upload result to GitHub Code Scanning
uses: github/codeql–action/upload–sarif@v2
with:
sarif_file: snyk.sarif
– name: Monitor project (main branch only)
if: github.ref == 'refs/heads/main'
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
command: monitor
3. 依赖更新策略
3.1 版本管理策略
// scripts/update-deps.js
const { exec } = require('child_process');
const util = require('util');
const fs = require('fs').promises;
const execPromise = util.promisify(exec);
class DependencyManager {
constructor() {
this.ignoredPackages = [];
this.updateStrategy = 'conservative'; // conservative, moderate, aggressive
}
async checkOutdated() {
const { stdout } = await execPromise('npm outdated –json');
return JSON.parse(stdout || '{}');
}
async getPackageInfo(packageName) {
try {
const { stdout } = await execPromise(`npm view ${packageName} –json`);
return JSON.parse(stdout);
} catch (error) {
return null;
}
}
async analyzeUpdates() {
const outdated = await this.checkOutdated();
const analysis = {};
for (const [pkg, info] of Object.entries(outdated)) {
const pkgInfo = await this.getPackageInfo(pkg);
analysis[pkg] = {
current: info.current,
wanted: info.wanted,
latest: info.latest,
type: this.getUpdateType(info.current, info.latest),
breaking: this.isBreakingUpdate(info.current, info.latest),
hasSecurityPatch: await this.hasSecurityPatch(pkg, info.current),
recommended: this.getRecommendedVersion(info)
};
}
return analysis;
}
getUpdateType(current, latest) {
const currentParts = current.split('.').map(Number);
const latestParts = latest.split('.').map(Number);
if (latestParts[0] > currentParts[0]) return 'major';
if (latestParts[1] > currentParts[1]) return 'minor';
if (latestParts[2] > currentParts[2]) return 'patch';
return 'none';
}
isBreakingUpdate(current, latest) {
const currentMajor = parseInt(current.split('.')[0]);
const latestMajor = parseInt(latest.split('.')[0]);
return latestMajor > currentMajor;
}
async hasSecurityPatch(packageName, currentVersion) {
try {
const { stdout } = await execPromise(`npm audit –json`);
const audit = JSON.parse(stdout);
for (const [pkg, data] of Object.entries(audit.vulnerabilities || {})) {
if (pkg === packageName) {
return data.fixAvailable !== false;
}
}
return false;
} catch {
return false;
}
}
getRecommendedVersion(info) {
// 保守策略: 只更新补丁版本
if (this.updateStrategy === 'conservative') {
return info.wanted;
}
// 中等策略: 更新次要版本
if (this.updateStrategy === 'moderate') {
const parts = info.latest.split('.');
parts[0] = info.current.split('.')[0];
return parts.join('.');
}
// 激进策略: 更新到最新
return info.latest;
}
async updatePackage(packageName, version) {
console.log(`📦 更新 ${packageName} -> ${version}`);
try {
await execPromise(`npm install ${packageName}@${version} –save`);
console.log(`✅ ${packageName} 更新成功`);
return true;
} catch (error) {
console.error(`❌ ${packageName} 更新失败:`, error.message);
return false;
}
}
async batchUpdate(analysis) {
const results = {
updated: [],
failed: [],
skipped: []
};
for (const [pkg, info] of Object.entries(analysis)) {
// 跳过忽略的包
if (this.ignoredPackages.includes(pkg)) {
results.skipped.push(pkg);
continue;
}
// 安全补丁优先
if (info.hasSecurityPatch) {
const success = await this.updatePackage(pkg, info.recommended);
if (success) results.updated.push(pkg);
else results.failed.push(pkg);
continue;
}
// 非主要版本更新自动更新
if (!info.breaking && this.updateStrategy !== 'conservative') {
const success = await this.updatePackage(pkg, info.recommended);
if (success) results.updated.push(pkg);
else results.failed.push(pkg);
continue;
}
results.skipped.push(pkg);
}
return results;
}
async generateUpdateReport() {
const analysis = await this.analyzeUpdates();
console.log('\\n=== 依赖更新报告 ===\\n');
const securityUpdates = [];
const majorUpdates = [];
const minorUpdates = [];
const patchUpdates = [];
for (const [pkg, info] of Object.entries(analysis)) {
if (info.hasSecurityPatch) {
securityUpdates.push(pkg);
} else if (info.type === 'major') {
majorUpdates.push(pkg);
} else if (info.type === 'minor') {
minorUpdates.push(pkg);
} else if (info.type === 'patch') {
patchUpdates.push(pkg);
}
}
console.log(`🔒 安全更新 (${securityUpdates.length}):`);
securityUpdates.forEach(pkg => console.log(` – ${pkg}`));
console.log(`\\n⚠️ 主要版本更新 (${majorUpdates.length}):`);
majorUpdates.forEach(pkg => console.log(` – ${pkg}`));
console.log(`\\n📈 次要版本更新 (${minorUpdates.length}):`);
minorUpdates.forEach(pkg => console.log(` – ${pkg}`));
console.log(`\\n🐛 补丁版本更新 (${patchUpdates.length}):`);
patchUpdates.forEach(pkg => console.log(` – ${pkg}`));
return analysis;
}
}
// Dependabot 配置
// .github/dependabot.yml
/*
version: 2
updates:
– package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "Asia/Shanghai"
open-pull-requests-limit: 10
versioning-strategy: "increase"
labels:
– "dependencies"
– "security"
commit-message:
prefix: "chore(deps)"
prefix-development: "chore(dev-deps)"
ignore:
– dependency-name: "express"
versions: ["5.x"]
groups:
production-dependencies:
dependency-type: "production"
development-dependencies:
dependency-type: "development"
patterns:
– "eslint*"
– "jest*"
reviewers:
– "team-backend"
assignees:
– "maintainer"
*/
3.2 依赖锁定与验证
// scripts/verify-deps.js
const crypto = require('crypto');
const fs = require('fs').promises;
const path = require('path');
class DependencyVerifier {
constructor() {
this.integrityHashes = new Map();
this.signatures = new Map();
}
async generateIntegrityHash(packageName, version) {
const packagePath = path.join('node_modules', packageName);
const files = await this.getAllFiles(packagePath);
const hash = crypto.createHash('sha256');
for (const file of files) {
const content = await fs.readFile(file);
hash.update(content);
}
const integrity = hash.digest('base64');
this.integrityHashes.set(`${packageName}@${version}`, integrity);
return integrity;
}
async getAllFiles(dir) {
const files = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (!['node_modules', '.git'].includes(entry.name)) {
files.push(…await this.getAllFiles(fullPath));
}
} else if (entry.isFile() && !entry.name.endsWith('.map')) {
files.push(fullPath);
}
}
return files;
}
async lockIntegrity() {
const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8'));
const lockfile = {};
const allDeps = {
…packageJson.dependencies,
…packageJson.devDependencies
};
for (const [name, version] of Object.entries(allDeps)) {
const integrity = await this.generateIntegrityHash(name, version);
lockfile[`${name}@${version}`] = { integrity, version };
}
await fs.writeFile('integrity.lock', JSON.stringify(lockfile, null, 2));
console.log('✅ 完整性锁定文件已生成');
}
async verifyIntegrity() {
const lockfile = JSON.parse(await fs.readFile('integrity.lock', 'utf8'));
let isValid = true;
for (const [pkg, expected] of Object.entries(lockfile)) {
const actual = await this.generateIntegrityHash(
pkg.split('@')[0],
expected.version
);
if (actual !== expected.integrity) {
console.error(`❌ 完整性验证失败: ${pkg}`);
isValid = false;
}
}
if (isValid) {
console.log('✅ 所有依赖完整性验证通过');
}
return isValid;
}
}
练习题
基础题
进阶题
挑战题
练习题参考答案
基础题1
npm audit
npm audit fix
基础题2
snyk test
snyk monitor
基础题3
npm outdated




