From 4b0f476a77d4c89164f52aa54301f7477ae7d12f Mon Sep 17 00:00:00 2001 From: Giovanni Zadinello Date: Fri, 22 Aug 2025 11:08:36 -0300 Subject: [PATCH 1/4] Create index.js --- index.js | 251 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 index.js diff --git a/index.js b/index.js new file mode 100644 index 0000000..a766255 --- /dev/null +++ b/index.js @@ -0,0 +1,251 @@ +const core = require('@actions/core'); +const exec = require('@actions/exec'); +const fs = require('fs'); +const path = require('path'); + +/** + * Vulnify GitHub Action + * Executes vulnerability scanning using Vulnify CLI + */ + +async function run() { + try { + // Get inputs + const file = core.getInput('file'); + const ecosystem = core.getInput('ecosystem'); + const output = core.getInput('output') || 'table'; + const severity = core.getInput('severity'); + const apiKey = core.getInput('api-key'); + const timeout = core.getInput('timeout') || '30000'; + const failOn = core.getInput('fail-on') || 'high'; + const workingDirectory = core.getInput('working-directory') || '.'; + const generateReport = core.getInput('generate-report') === 'true'; + const reportFilename = core.getInput('report-filename') || 'vulnify-report.json'; + + core.info('🔍 Starting Vulnify security scan...'); + + // Change to working directory + if (workingDirectory !== '.') { + process.chdir(workingDirectory); + core.info(`📁 Changed to working directory: ${workingDirectory}`); + } + + // Install Vulnify CLI globally + core.info('đŸ“Ļ Installing Vulnify CLI...'); + await exec.exec('npm', ['install', '-g', 'vulnify']); + + // Build command arguments + const args = ['test']; + + if (file) { + args.push('--file', file); + } + + if (ecosystem) { + args.push('--ecosystem', ecosystem); + } + + if (severity) { + args.push('--severity', severity); + } + + if (apiKey) { + args.push('--api-key', apiKey); + } + + if (timeout) { + args.push('--timeout', timeout); + } + + // Always generate JSON output for parsing + args.push('--output', 'json'); + + if (!generateReport) { + args.push('--no-report'); + } + + core.info(`🚀 Running: vulnify ${args.join(' ')}`); + + // Execute Vulnify scan + let vulnifyOutput = ''; + let vulnifyError = ''; + + const options = { + listeners: { + stdout: (data) => { + vulnifyOutput += data.toString(); + }, + stderr: (data) => { + vulnifyError += data.toString(); + } + }, + ignoreReturnCode: true // Don't fail on non-zero exit codes + }; + + const exitCode = await exec.exec('vulnify', args, options); + + // Parse results + let scanResults = null; + let reportPath = ''; + + try { + // Try to parse JSON output + if (vulnifyOutput.trim()) { + const jsonMatch = vulnifyOutput.match(/\{[\s\S]*\}/); + if (jsonMatch) { + scanResults = JSON.parse(jsonMatch[0]); + } + } + + // Check for report file + const possibleReportPaths = [ + reportFilename, + 'vulnify-report.json', + 'report.json' + ]; + + for (const reportFile of possibleReportPaths) { + if (fs.existsSync(reportFile)) { + reportPath = path.resolve(reportFile); + if (!scanResults) { + scanResults = JSON.parse(fs.readFileSync(reportFile, 'utf8')); + } + break; + } + } + } catch (parseError) { + core.warning(`Failed to parse scan results: ${parseError.message}`); + } + + // Extract metrics + let totalVulns = 0; + let criticalCount = 0; + let highCount = 0; + let mediumCount = 0; + let lowCount = 0; + + if (scanResults && scanResults.results && scanResults.results.summary) { + const summary = scanResults.results.summary; + criticalCount = summary.critical || 0; + highCount = summary.high || 0; + mediumCount = summary.medium || 0; + lowCount = summary.low || 0; + totalVulns = criticalCount + highCount + mediumCount + lowCount; + } else if (scanResults && scanResults.summary) { + const summary = scanResults.summary; + criticalCount = summary.critical || 0; + highCount = summary.high || 0; + mediumCount = summary.medium || 0; + lowCount = summary.low || 0; + totalVulns = criticalCount + highCount + mediumCount + lowCount; + } + + // Set outputs + core.setOutput('vulnerabilities-found', totalVulns.toString()); + core.setOutput('critical-count', criticalCount.toString()); + core.setOutput('high-count', highCount.toString()); + core.setOutput('medium-count', mediumCount.toString()); + core.setOutput('low-count', lowCount.toString()); + core.setOutput('report-path', reportPath); + + // Display results in a nice format + if (output !== 'json') { + core.info(''); + core.info('📊 Vulnerability Scan Results:'); + core.info('================================'); + core.info(`🔴 Critical: ${criticalCount}`); + core.info(`🟠 High: ${highCount}`); + core.info(`🟡 Medium: ${mediumCount}`); + core.info(`đŸŸĸ Low: ${lowCount}`); + core.info(`📈 Total: ${totalVulns}`); + core.info(''); + } + + if (reportPath) { + core.info(`📄 Report generated: ${reportPath}`); + } + + // Determine if build should fail + let shouldFail = false; + let failReason = ''; + + switch (failOn.toLowerCase()) { + case 'critical': + if (criticalCount > 0) { + shouldFail = true; + failReason = `Found ${criticalCount} critical vulnerabilities`; + } + break; + case 'high': + if (criticalCount > 0 || highCount > 0) { + shouldFail = true; + failReason = `Found ${criticalCount} critical and ${highCount} high severity vulnerabilities`; + } + break; + case 'medium': + if (criticalCount > 0 || highCount > 0 || mediumCount > 0) { + shouldFail = true; + failReason = `Found vulnerabilities: ${criticalCount} critical, ${highCount} high, ${mediumCount} medium`; + } + break; + case 'low': + case 'any': + if (totalVulns > 0) { + shouldFail = true; + failReason = `Found ${totalVulns} vulnerabilities`; + } + break; + } + + if (shouldFail) { + core.setOutput('scan-result', 'fail'); + core.setFailed(`❌ Security scan failed: ${failReason}`); + } else { + core.setOutput('scan-result', 'pass'); + core.info('✅ Security scan passed - no vulnerabilities found above threshold'); + } + + // Handle Vulnify CLI errors + if (exitCode !== 0 && !shouldFail) { + if (vulnifyError) { + core.warning(`Vulnify CLI stderr: ${vulnifyError}`); + } + + // Don't fail if it's just vulnerabilities found + if (exitCode === 1 && totalVulns > 0) { + core.info('â„šī¸ Vulnify CLI exited with code 1 (vulnerabilities found)'); + } else if (exitCode === 2) { + core.setFailed(`❌ Vulnify CLI execution error (exit code 2)`); + } else { + core.warning(`âš ī¸ Vulnify CLI exited with code ${exitCode}`); + } + } + + } catch (error) { + core.setFailed(`❌ Action failed: ${error.message}`); + + // Provide helpful debugging information + core.info(''); + core.info('🔧 Debugging Information:'); + core.info('========================'); + core.info(`Working Directory: ${process.cwd()}`); + core.info(`Node Version: ${process.version}`); + core.info(`Platform: ${process.platform}`); + + // List files in current directory + try { + const files = fs.readdirSync('.'); + core.info(`Files in directory: ${files.join(', ')}`); + } catch (fsError) { + core.info('Could not list directory files'); + } + } +} + +// Execute the action +if (require.main === module) { + run(); +} + +module.exports = { run }; + From 90a68eeca8278947713b160f2c551add5b04ad2b Mon Sep 17 00:00:00 2001 From: Giovanni Zadinello Date: Fri, 22 Aug 2025 11:09:39 -0300 Subject: [PATCH 2/4] Create action.yml --- action.yml | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 action.yml diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..9e008db --- /dev/null +++ b/action.yml @@ -0,0 +1,85 @@ +name: 'Vulnify Security Scanner' +description: 'Scan your project dependencies for security vulnerabilities using Vulnify SCA - similar to Snyk CLI' +author: 'Vulnify Team' + +branding: + icon: 'shield' + color: 'red' + +inputs: + file: + description: 'Path to dependency file (package.json, requirements.txt, etc.)' + required: false + default: '' + + ecosystem: + description: 'Force specific ecosystem (npm, pypi, maven, nuget, rubygems, composer, go, cargo)' + required: false + default: '' + + output: + description: 'Output format (table, json, summary)' + required: false + default: 'table' + + severity: + description: 'Filter by severity level (critical, high, medium, low)' + required: false + default: '' + + api-key: + description: 'Vulnify API key for increased rate limits' + required: false + default: '' + + timeout: + description: 'Request timeout in milliseconds' + required: false + default: '30000' + + fail-on: + description: 'Fail the build on vulnerabilities (critical, high, medium, low, any)' + required: false + default: 'high' + + working-directory: + description: 'Working directory to run the scan' + required: false + default: '.' + + generate-report: + description: 'Generate JSON report file' + required: false + default: 'true' + + report-filename: + description: 'Name of the generated report file' + required: false + default: 'vulnify-report.json' + +outputs: + vulnerabilities-found: + description: 'Total number of vulnerabilities found' + + critical-count: + description: 'Number of critical vulnerabilities' + + high-count: + description: 'Number of high severity vulnerabilities' + + medium-count: + description: 'Number of medium severity vulnerabilities' + + low-count: + description: 'Number of low severity vulnerabilities' + + report-path: + description: 'Path to the generated JSON report' + + scan-result: + description: 'Overall scan result (pass/fail)' + +runs: + using: 'node20' + main: 'dist/index.js' + From 841e084fbc6a0246b7273ebe849b47ea69297949 Mon Sep 17 00:00:00 2001 From: Giovanni Zadinello Date: Fri, 22 Aug 2025 11:10:35 -0300 Subject: [PATCH 3/4] Create action_package.json --- action_package.json | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 action_package.json diff --git a/action_package.json b/action_package.json new file mode 100644 index 0000000..9087f8a --- /dev/null +++ b/action_package.json @@ -0,0 +1,41 @@ +{ + "name": "vulnify-action", + "version": "1.0.0", + "description": "GitHub Action for Vulnify Security Scanner", + "main": "dist/index.js", + "scripts": { + "build": "ncc build index.js -o dist --source-map --license licenses.txt", + "package": "npm run build", + "test": "jest", + "lint": "eslint index.js", + "all": "npm run lint && npm run build && npm test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vulnify/vulnify-cli.git" + }, + "keywords": [ + "security", + "vulnerability", + "scanner", + "github-action", + "sca", + "dependencies", + "audit" + ], + "author": "Vulnify Team", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/exec": "^1.1.1", + "@actions/io": "^1.1.3" + }, + "devDependencies": { + "@vercel/ncc": "^0.38.1", + "eslint": "^8.57.0", + "jest": "^29.7.0" + }, + "engines": { + "node": ">=20" + } +} From 79561c849f335022dd956de5b0786fbd8a682d6f Mon Sep 17 00:00:00 2001 From: Giovanni Zadinello Date: Fri, 22 Aug 2025 11:10:57 -0300 Subject: [PATCH 4/4] Create test-action.yml --- .github/workflows/test-action.yml | 261 ++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 .github/workflows/test-action.yml diff --git a/.github/workflows/test-action.yml b/.github/workflows/test-action.yml new file mode 100644 index 0000000..aad1ed8 --- /dev/null +++ b/.github/workflows/test-action.yml @@ -0,0 +1,261 @@ +name: Test Vulnify Action + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + test-action: + runs-on: ubuntu-latest + name: Test Vulnify Security Scanner Action + + strategy: + matrix: + test-case: + - name: "Basic npm scan" + ecosystem: "npm" + file: "package.json" + fail-on: "critical" + - name: "Python requirements scan" + ecosystem: "pypi" + file: "requirements.txt" + fail-on: "high" + - name: "Auto-detect scan" + ecosystem: "" + file: "" + fail-on: "medium" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create test dependency files + run: | + # Create package.json with known vulnerable packages for testing + cat > package.json << 'EOF' + { + "name": "test-project", + "version": "1.0.0", + "dependencies": { + "lodash": "4.17.19", + "axios": "0.21.1", + "express": "4.17.1" + } + } + EOF + + # Create requirements.txt with known vulnerable packages + cat > requirements.txt << 'EOF' + django==2.2.0 + requests==2.20.0 + pillow==6.2.0 + EOF + + # Create pom.xml for Maven testing + cat > pom.xml << 'EOF' + + + 4.0.0 + com.example + test-project + 1.0.0 + + + org.apache.struts + struts2-core + 2.3.20 + + + + EOF + + - name: Test Vulnify Action - ${{ matrix.test-case.name }} + id: vulnify-test + uses: ./ + continue-on-error: true + with: + ecosystem: ${{ matrix.test-case.ecosystem }} + file: ${{ matrix.test-case.file }} + fail-on: ${{ matrix.test-case.fail-on }} + output: 'table' + generate-report: true + report-filename: 'test-report-${{ strategy.job-index }}.json' + timeout: '45000' + + - name: Display test results + run: | + echo "## Test Results for: ${{ matrix.test-case.name }}" + echo "Vulnerabilities found: ${{ steps.vulnify-test.outputs.vulnerabilities-found }}" + echo "Critical: ${{ steps.vulnify-test.outputs.critical-count }}" + echo "High: ${{ steps.vulnify-test.outputs.high-count }}" + echo "Medium: ${{ steps.vulnify-test.outputs.medium-count }}" + echo "Low: ${{ steps.vulnify-test.outputs.low-count }}" + echo "Scan result: ${{ steps.vulnify-test.outputs.scan-result }}" + echo "Report path: ${{ steps.vulnify-test.outputs.report-path }}" + + - name: Verify report generation + run: | + if [ -f "test-report-${{ strategy.job-index }}.json" ]; then + echo "✅ Report file generated successfully" + echo "Report size: $(wc -c < test-report-${{ strategy.job-index }}.json) bytes" + echo "Report preview:" + head -20 "test-report-${{ strategy.job-index }}.json" + else + echo "❌ Report file not found" + ls -la + fi + + - name: Upload test reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: vulnify-test-reports-${{ strategy.job-index }} + path: | + test-report-*.json + vulnify-report.json + retention-days: 7 + + test-different-fail-conditions: + runs-on: ubuntu-latest + name: Test Different Fail Conditions + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create vulnerable package.json + run: | + cat > package.json << 'EOF' + { + "name": "vulnerable-test", + "version": "1.0.0", + "dependencies": { + "lodash": "4.17.19" + } + } + EOF + + - name: Test fail-on critical (should pass) + uses: ./ + with: + fail-on: 'critical' + output: 'summary' + + - name: Test fail-on any (should fail) + uses: ./ + continue-on-error: true + with: + fail-on: 'any' + output: 'json' + + test-with-api-key: + runs-on: ubuntu-latest + name: Test with API Key + if: github.event_name != 'pull_request' # Only run on push/manual + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create test package.json + run: | + cat > package.json << 'EOF' + { + "name": "api-key-test", + "version": "1.0.0", + "dependencies": { + "express": "4.17.1" + } + } + EOF + + - name: Test with API key + uses: ./ + with: + api-key: ${{ secrets.VULNIFY_API_KEY }} + fail-on: 'high' + timeout: '60000' + + test-working-directory: + runs-on: ubuntu-latest + name: Test Working Directory + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create subdirectory with dependencies + run: | + mkdir -p frontend backend + + cat > frontend/package.json << 'EOF' + { + "name": "frontend-app", + "version": "1.0.0", + "dependencies": { + "react": "17.0.2", + "lodash": "4.17.19" + } + } + EOF + + cat > backend/requirements.txt << 'EOF' + django==2.2.0 + requests==2.20.0 + EOF + + - name: Test frontend scan + uses: ./ + with: + working-directory: './frontend' + ecosystem: 'npm' + fail-on: 'critical' + + - name: Test backend scan + uses: ./ + with: + working-directory: './backend' + ecosystem: 'pypi' + fail-on: 'critical' + + integration-test: + runs-on: ubuntu-latest + name: Integration Test with Real Project + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Test on actual CLI project + uses: ./ + with: + file: 'package.json' + ecosystem: 'npm' + fail-on: 'critical' + generate-report: true + report-filename: 'cli-security-report.json' + + - name: Validate CLI scan results + run: | + if [ -f "cli-security-report.json" ]; then + echo "✅ CLI project scan completed successfully" + + # Parse and display key metrics + if command -v jq &> /dev/null; then + echo "Project dependencies scanned: $(jq -r '.results.total_dependencies // .total_dependencies // "unknown"' cli-security-report.json)" + echo "Vulnerabilities found: $(jq -r '.results.vulnerabilities_found // .vulnerabilities_found // "unknown"' cli-security-report.json)" + fi + else + echo "❌ CLI project scan failed" + exit 1 + fi + + - name: Upload CLI scan report + uses: actions/upload-artifact@v4 + with: + name: cli-security-report + path: cli-security-report.json +