Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cli/generators/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ func GenerateGithubActions(cfg *InitConfig) error {
tmplName = "workflows/node_security.yml.tmpl"
case "golang":
tmplName = "workflows/go_security.yml.tmpl"
case "python":
tmplName = "workflows/python_security.yml.tmpl"
case "java":
tmplName = "workflows/java_security.yml.tmpl"
default:
return fmt.Errorf("no workflow template for language: %s", cfg.Project.Language)
}
Expand Down
275 changes: 275 additions & 0 deletions cli/templates/workflows/java_security.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
name: Security Scan

on:
push:
branches: ["main"]
pull_request:

permissions:
contents: read
issues: write
pull-requests: write

jobs:
security:
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Prepare artifacts directory
run: mkdir -p artifacts/security

- name: Extract exclude paths from config
id: exclude-paths
run: |
python3 << 'EOF'
import yaml
import json
import os

exclude_paths = []

try:
if os.path.exists('security-config.yml'):
with open('security-config.yml') as f:
config = yaml.safe_load(f)
if config and 'exclude_paths' in config:
exclude_paths = config['exclude_paths'] or []
except Exception as e:
print(f"Error reading config: {e}")

semgrep_excludes = ' '.join([f'--exclude {p}' for p in exclude_paths])
gitleaks_excludes = ','.join(exclude_paths) if exclude_paths else ''

with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write(f"semgrep_excludes={semgrep_excludes}\n")
f.write(f"gitleaks_excludes={gitleaks_excludes}\n")
f.write(f"exclude_paths={json.dumps(exclude_paths)}\n")
EOF
pip install pyyaml

- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "temurin"

- name: Cache Maven packages
if: hashFiles('pom.xml') != ''
uses: actions/cache@v4
with:
path: ~/.m2
key: ${{"{{"}} runner.os {{ "}}" }}-m2-${{"{{"}} hashFiles('**/pom.xml') {{ "}}" }}

- name: Cache Gradle packages
if: hashFiles('build.gradle') != '' || hashFiles('build.gradle.kts') != ''
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{"{{"}} runner.os {{ "}}" }}-gradle-${{"{{"}} hashFiles('**/*.gradle*') {{ "}}" }}

- name: Install dependencies (Maven)
if: hashFiles('pom.xml') != ''
run: mvn dependency:resolve -q

- name: Install dependencies (Gradle)
if: hashFiles('build.gradle') != '' || hashFiles('build.gradle.kts') != ''
run: ./gradlew dependencies -q || gradle dependencies -q

{{- if .Tools.Semgrep }}
- name: Set up Python for Semgrep
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Run Semgrep
run: |
pip install semgrep
semgrep --config p/ci ${{"{{"}} steps.exclude-paths.outputs.semgrep_excludes {{ "}}" }} --json > artifacts/security/semgrep-report.json || true
{{- end }}

{{- if .Tools.Gitleaks }}
- name: Create Gitleaks config with exclusions
if: steps.exclude-paths.outputs.gitleaks_excludes != ''
run: |
cat > .gitleaks.toml << 'EOF'
[allowlist]
paths = [
'''${{"{{"}} steps.exclude-paths.outputs.gitleaks_excludes {{ "}}" }}'''
]
EOF
sed -i "s/'''/\"/g" .gitleaks.toml
sed -i 's/,/",\n "/g' .gitleaks.toml

- name: Install Gitleaks
run: |
curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.18.1/gitleaks_8.18.1_linux_x64.tar.gz | tar -xz
sudo mv gitleaks /usr/local/bin/

- name: Run Gitleaks
run: |
if [ -f .gitleaks.toml ]; then
gitleaks detect --report-format json --report-path artifacts/security/gitleaks-report.json --config .gitleaks.toml --exit-code 0 || true
else
gitleaks detect --report-format json --report-path artifacts/security/gitleaks-report.json --exit-code 0 || true
fi
{{- end }}

{{- if .Tools.Trivy }}
- name: Run Trivy FS Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
format: json
output: artifacts/security/trivy-fs.json
severity: HIGH,CRITICAL
skip-dirs: ${{"{{"}} steps.exclude-paths.outputs.gitleaks_excludes {{ "}}" }}

- name: Check for Dockerfile
id: docker-check
run: |
if [ -f "Dockerfile" ]; then
echo "has_docker=true" >> $GITHUB_OUTPUT
else
echo "has_docker=false" >> $GITHUB_OUTPUT
fi

- name: Build Docker image for scanning
if: steps.docker-check.outputs.has_docker == 'true'
run: docker build -t devsecops-scan-temp:latest .

- name: Run Trivy Image Scan
if: steps.docker-check.outputs.has_docker == 'true'
uses: aquasecurity/trivy-action@master
with:
scan-type: image
image-ref: devsecops-scan-temp:latest
format: json
output: artifacts/security/trivy-image.json
severity: HIGH,CRITICAL
{{- end }}

- name: Extract fail_on thresholds from config
if: always()
run: |
pip install pyyaml
python3 << 'EOF'
import yaml
import json
import os

fail_on = {
'gitleaks': 0, 'semgrep': 10,
'trivy_critical': 0, 'trivy_high': 5,
'trivy_medium': -1, 'trivy_low': -1
}

try:
if os.path.exists('security-config.yml'):
with open('security-config.yml') as f:
config = yaml.safe_load(f)
if config and 'fail_on' in config:
fail_on.update(config['fail_on'])
except Exception as e:
print(f"Error reading config, using defaults: {e}")

os.makedirs('artifacts/security', exist_ok=True)
with open('artifacts/security/fail-on.json', 'w') as out:
json.dump(fail_on, out)
EOF

- name: Generate security summary (JSON)
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const dir = 'artifacts/security';

function readJson(file) {
if (!fs.existsSync(file)) return null;
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
}

const failOn = readJson(path.join(dir, 'fail-on.json')) || { gitleaks: 0, semgrep: 10, trivy_critical: 0, trivy_high: 5, trivy_medium: -1, trivy_low: -1 };
const result = { version: "0.3.0", status: "PASS", blocking_count: 0, summary: {} };

const gitleaks = readJson(path.join(dir, 'gitleaks-report.json'));
if (gitleaks) {
const count = Array.isArray(gitleaks) ? gitleaks.length : (gitleaks.findings?.length || 0);
result.summary.gitleaks = { total: count };
if (failOn.gitleaks >= 0 && count > failOn.gitleaks) result.blocking_count += count - failOn.gitleaks;
}

const trivy = readJson(path.join(dir, 'trivy-fs.json'));
if (trivy?.Results) {
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
for (const r of trivy.Results) for (const v of (r.Vulnerabilities || [])) { const s = v.Severity?.toLowerCase(); if (s in counts) counts[s]++; }
result.summary.trivy_fs = counts;
for (const [s, k] of [['critical','trivy_critical'],['high','trivy_high'],['medium','trivy_medium'],['low','trivy_low']]) {
if (failOn[k] >= 0 && counts[s] > failOn[k]) result.blocking_count += counts[s] - failOn[k];
}
}

const semgrep = readJson(path.join(dir, 'semgrep-report.json'));
if (semgrep) {
const count = (Array.isArray(semgrep) ? semgrep : (semgrep.results || [])).length;
result.summary.semgrep = { total: count };
if (failOn.semgrep >= 0 && count > failOn.semgrep) result.blocking_count += count - failOn.semgrep;
}

result.status = result.blocking_count > 0 ? "FAIL" : "PASS";
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'summary.json'), JSON.stringify(result, null, 2));

- name: Post PR Security Summary
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
github-token: ${{"{{"}} secrets.GITHUB_TOKEN {{ "}}" }}
script: |
const fs = require('fs');
const marker = '<!-- devsecops-kit-security-summary -->';
let summary = null;
try { summary = JSON.parse(fs.readFileSync('artifacts/security/summary.json', 'utf8')); } catch {}

let body = `${marker}\n### 🔐 DevSecOps Kit Security Summary\n\n`;
if (!summary) { body += "_No summary available._\n"; } else {
body += `- **Gitleaks:** ${summary.summary?.gitleaks?.total ?? 0} leak(s)\n`;
const trivyFs = summary.summary?.trivy_fs ?? {};
if (Object.keys(trivyFs).length > 0) { body += `- **Trivy FS:**\n`; for (const s of Object.keys(trivyFs)) body += ` - ${s.toUpperCase()}: ${trivyFs[s]}\n`; }
if (summary.summary?.semgrep) body += `- **Semgrep:** ${summary.summary.semgrep.total} finding(s)\n`;
body += `\n**Status:** ${summary.status === "FAIL" ? '🚨 **FAIL**' : '✅ **PASS**'}\n`;
if (summary.blocking_count > 0) body += `_${summary.blocking_count} issue(s) exceed configured thresholds_\n`;
}

const { owner, repo } = context.repo;
const pr = context.issue.number;
const comments = await github.rest.issues.listComments({ owner, repo, issue_number: pr });
const existing = comments.data.find(c => c.body?.includes(marker));
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: pr, body });
}

- name: Upload security artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: security-reports
path: artifacts/security/

- name: Check fail gates
if: always()
run: |
STATUS=$(cat artifacts/security/summary.json | python3 -c "import sys, json; print(json.load(sys.stdin).get('status', 'UNKNOWN'))")
BLOCKING=$(cat artifacts/security/summary.json | python3 -c "import sys, json; print(json.load(sys.stdin).get('blocking_count', 0))")
echo "Status: $STATUS | Blocking: $BLOCKING"
if [ "$STATUS" = "FAIL" ]; then echo "❌ Security scan FAILED: $BLOCKING issue(s) exceed thresholds"; exit 1; fi
echo "✅ Security scan PASSED"
Loading
Loading