mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 03:23:59 +00:00
feat: use our own actions in our workflows (#377)
* feat: use our own actions in our workflows * fix: add missing inputs to validate-inputs, refactor node * chore: cr comment fixes * fix: update-validators formatting * chore: update validators, add tests, conventions * feat: validate severity with severity_enum * feat: add 10 generic validators to improve input validation coverage Add comprehensive validation system improvements across multiple phases: Phase 2A - Quick Wins: - Add multi_value_enum validator for 2-10 value enumerations - Add exit_code_list validator for Unix/Linux exit codes (0-255) - Refactor coverage_driver to use multi_value_enum Phase 2B - High-Value Validators: - Add key_value_list validator with shell injection prevention - Add path_list validator with path traversal and glob support Quick Wins - Additional Enums: - Add network_mode validator for Docker network modes - Add language_enum validator for language detection - Add framework_mode validator for PHP framework modes - Update boolean pattern to include 'push' Phase 2C - Specialized Validators: - Add json_format validator for JSON syntax validation - Add cache_config validator for Docker BuildKit cache configs Improvements: - All validators include comprehensive security checks - Pattern-based validation with clear error messages - 23 new test methods with edge case coverage - Update special case mappings for 20+ inputs - Fix build-args mapping test expectation Coverage impact: 22 actions now at 100% validation (88% → 92%) Test suite: 762 → 785 tests (+23 tests, all passing) * chore: regenerate rules.yml with improved validator coverage Regenerate validation rules for all actions with new validators: - compress-images: 86% → 100% (+1 input: ignore-paths) - docker-build: 63% → 100% (+4 inputs: cache configs, platform-build-args) - docker-publish: 73% → 100% (+1 input: build-args) - language-version-detect: 67% → 100% (+1 input: language) - php-tests: 89% (fixed framework→framework_mode mapping) - prettier-lint: 86% → 100% (+2 inputs: file-pattern, plugins) - security-scan: 86% (maintained coverage) Overall: 23 of 25 actions now at 100% validation coverage (92%) * fix: address PR #377 review comments - Add | None type annotations to 6 optional parameters (PEP 604) - Standardize injection pattern: remove @# from comma_separated_list validator (@ and # are not shell injection vectors, allows npm scoped packages) - Remove dead code: unused value expression in key_value_list validator - Update tests to reflect injection pattern changes
This commit is contained in:
206
.github/workflows/action-security.yml
vendored
206
.github/workflows/action-security.yml
vendored
@@ -39,212 +39,30 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Check Required Configurations
|
- name: Run Security Scan
|
||||||
id: check-configs
|
id: security-scan
|
||||||
shell: sh
|
uses: ./security-scan
|
||||||
run: |
|
|
||||||
# Initialize all flags as false
|
|
||||||
{
|
|
||||||
echo "run_gitleaks=false"
|
|
||||||
echo "run_trivy=true"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Check Gitleaks configuration and license
|
|
||||||
if [ -f ".gitleaks.toml" ] && [ -n "${{ secrets.GITLEAKS_LICENSE }}" ]; then
|
|
||||||
echo "Gitleaks config and license found"
|
|
||||||
printf '%s\n' "run_gitleaks=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "::warning::Gitleaks config or license missing - skipping Gitleaks scan"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run actionlint
|
|
||||||
uses: raven-actions/actionlint@3a24062651993d40fed1019b58ac6fbdfbf276cc # v2.0.1
|
|
||||||
with:
|
with:
|
||||||
cache: true
|
gitleaks-license: ${{ secrets.GITLEAKS_LICENSE }}
|
||||||
fail-on-error: true
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
shellcheck: false
|
|
||||||
|
|
||||||
- name: Run Gitleaks
|
|
||||||
if: steps.check-configs.outputs.run_gitleaks == 'true'
|
|
||||||
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
|
||||||
with:
|
|
||||||
config-path: .gitleaks.toml
|
|
||||||
report-format: sarif
|
|
||||||
report-path: gitleaks-report.sarif
|
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
|
||||||
uses: aquasecurity/trivy-action@a11da62073708815958ea6d84f5650c78a3ef85b # master
|
|
||||||
with:
|
|
||||||
scan-type: 'fs'
|
|
||||||
scanners: 'vuln,config,secret'
|
|
||||||
format: 'sarif'
|
|
||||||
output: 'trivy-results.sarif'
|
|
||||||
severity: 'CRITICAL,HIGH'
|
|
||||||
timeout: '10m'
|
|
||||||
|
|
||||||
- name: Verify SARIF files
|
|
||||||
id: verify-sarif
|
|
||||||
shell: sh
|
|
||||||
run: |
|
|
||||||
# Initialize outputs
|
|
||||||
{
|
|
||||||
echo "has_trivy=false"
|
|
||||||
echo "has_gitleaks=false"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Check Trivy results
|
|
||||||
if [ -f "trivy-results.sarif" ]; then
|
|
||||||
if jq -e . </dev/null 2>&1 <"trivy-results.sarif"; then
|
|
||||||
printf '%s\n' "has_trivy=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "::warning::Trivy SARIF file exists but is not valid JSON"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check Gitleaks results if it ran
|
|
||||||
if [ "${{ steps.check-configs.outputs.run_gitleaks }}" = "true" ]; then
|
|
||||||
if [ -f "gitleaks-report.sarif" ]; then
|
|
||||||
if jq -e . </dev/null 2>&1 <"gitleaks-report.sarif"; then
|
|
||||||
printf '%s\n' "has_gitleaks=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "::warning::Gitleaks SARIF file exists but is not valid JSON"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload Trivy results
|
|
||||||
if: steps.verify-sarif.outputs.has_trivy == 'true'
|
|
||||||
uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
|
||||||
with:
|
|
||||||
sarif_file: 'trivy-results.sarif'
|
|
||||||
category: 'trivy'
|
|
||||||
|
|
||||||
- name: Upload Gitleaks results
|
|
||||||
if: steps.verify-sarif.outputs.has_gitleaks == 'true'
|
|
||||||
uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
|
||||||
with:
|
|
||||||
sarif_file: 'gitleaks-report.sarif'
|
|
||||||
category: 'gitleaks'
|
|
||||||
|
|
||||||
- name: Archive security reports
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
with:
|
|
||||||
name: security-reports-${{ github.run_id }}
|
|
||||||
path: |
|
|
||||||
${{ steps.verify-sarif.outputs.has_trivy == 'true' && 'trivy-results.sarif' || '' }}
|
|
||||||
${{ steps.verify-sarif.outputs.has_gitleaks == 'true' && 'gitleaks-report.sarif' || '' }}
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
- name: Analyze Results
|
|
||||||
if: always()
|
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
try {
|
|
||||||
let totalIssues = 0;
|
|
||||||
let criticalIssues = 0;
|
|
||||||
|
|
||||||
const analyzeSarif = (file, tool) => {
|
|
||||||
if (!fs.existsSync(file)) {
|
|
||||||
console.log(`No results file found for ${tool}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sarif = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
||||||
return sarif.runs.reduce((acc, run) => {
|
|
||||||
if (!run.results) return acc;
|
|
||||||
|
|
||||||
const critical = run.results.filter(r =>
|
|
||||||
r.level === 'error' ||
|
|
||||||
r.level === 'critical' ||
|
|
||||||
(r.ruleId || '').toLowerCase().includes('critical')
|
|
||||||
).length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
total: acc.total + run.results.length,
|
|
||||||
critical: acc.critical + critical
|
|
||||||
};
|
|
||||||
}, { total: 0, critical: 0 });
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error analyzing ${tool} results: ${error.message}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only analyze results from tools that ran successfully
|
|
||||||
const results = {
|
|
||||||
trivy: ${{ steps.verify-sarif.outputs.has_trivy }} ?
|
|
||||||
analyzeSarif('trivy-results.sarif', 'trivy') : null,
|
|
||||||
gitleaks: ${{ steps.verify-sarif.outputs.has_gitleaks }} ?
|
|
||||||
analyzeSarif('gitleaks-report.sarif', 'gitleaks') : null
|
|
||||||
};
|
|
||||||
|
|
||||||
// Aggregate results
|
|
||||||
Object.entries(results).forEach(([tool, result]) => {
|
|
||||||
if (result) {
|
|
||||||
totalIssues += result.total;
|
|
||||||
criticalIssues += result.critical;
|
|
||||||
console.log(`${tool}: ${result.total} total, ${result.critical} critical issues`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create summary
|
|
||||||
const summary = `## Security Scan Summary
|
|
||||||
|
|
||||||
- Total Issues Found: ${totalIssues}
|
|
||||||
- Critical Issues: ${criticalIssues}
|
|
||||||
|
|
||||||
### Tool Breakdown
|
|
||||||
${Object.entries(results)
|
|
||||||
.filter(([_, r]) => r)
|
|
||||||
.map(([tool, r]) =>
|
|
||||||
`- ${tool}: ${r.total} total, ${r.critical} critical`
|
|
||||||
).join('\n')}
|
|
||||||
|
|
||||||
### Tools Run Status
|
|
||||||
- Trivy: ${{ steps.verify-sarif.outputs.has_trivy }}
|
|
||||||
- Gitleaks: ${{ steps.check-configs.outputs.run_gitleaks }}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Set output
|
|
||||||
core.setOutput('total_issues', totalIssues);
|
|
||||||
core.setOutput('critical_issues', criticalIssues);
|
|
||||||
|
|
||||||
// Add job summary
|
|
||||||
await core.summary
|
|
||||||
.addRaw(summary)
|
|
||||||
.write();
|
|
||||||
|
|
||||||
// Fail if critical issues found
|
|
||||||
if (criticalIssues > 0) {
|
|
||||||
core.setFailed(`Found ${criticalIssues} critical security issues`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
core.setFailed(`Analysis failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Notify on Critical Issues
|
- name: Notify on Critical Issues
|
||||||
if: failure()
|
if: failure() && steps.security-scan.outputs.critical_issues != '0'
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |-
|
script: |-
|
||||||
const { repo, owner } = context.repo;
|
const { repo, owner } = context.repo;
|
||||||
const critical = core.getInput('critical_issues');
|
const critical = '${{ steps.security-scan.outputs.critical_issues }}';
|
||||||
|
const total = '${{ steps.security-scan.outputs.total_issues }}';
|
||||||
|
|
||||||
const body = `🚨 Critical security issues found in GitHub Actions
|
const body = `🚨 Critical security issues found in GitHub Actions
|
||||||
|
|
||||||
${critical} critical security issues were found during the security scan.
|
${critical} critical security issues (out of ${total} total) were found during the security scan.
|
||||||
|
|
||||||
### Scan Results
|
### Scan Results
|
||||||
- Trivy: ${{ steps.verify-sarif.outputs.has_trivy == 'true' && 'Completed' || 'Skipped/Failed' }}
|
- Actionlint: Completed
|
||||||
- Gitleaks: ${{ steps.check-configs.outputs.run_gitleaks == 'true' && 'Completed' || 'Skipped' }}
|
- Trivy: ${{ steps.security-scan.outputs.has_trivy_results == 'true' && 'Completed' || 'Skipped/Failed' }}
|
||||||
|
- Gitleaks: ${{ steps.security-scan.outputs.has_gitleaks_results == 'true' && 'Completed' || 'Skipped' }}
|
||||||
|
|
||||||
[View detailed scan results](https://github.com/${owner}/${repo}/actions/runs/${context.runId})
|
[View detailed scan results](https://github.com/${owner}/${repo}/actions/runs/${context.runId})
|
||||||
|
|
||||||
|
|||||||
51
.github/workflows/codeql.yml
vendored
51
.github/workflows/codeql.yml
vendored
@@ -1,51 +0,0 @@
|
|||||||
---
|
|
||||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
|
||||||
name: 'CodeQL'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'main'
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- 'main'
|
|
||||||
schedule:
|
|
||||||
- cron: '30 1 * * 0' # Run at 1:30 AM UTC every Sunday
|
|
||||||
merge_group:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language:
|
|
||||||
- 'actions'
|
|
||||||
- 'javascript'
|
|
||||||
- 'python'
|
|
||||||
|
|
||||||
steps: # Add languages used in your actions
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
queries: security-and-quality
|
|
||||||
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
|
||||||
with:
|
|
||||||
category: '/language:${{matrix.language}}'
|
|
||||||
110
.github/workflows/pr-lint.yml
vendored
110
.github/workflows/pr-lint.yml
vendored
@@ -24,17 +24,9 @@ on:
|
|||||||
merge_group:
|
merge_group:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Apply linter fixes configuration
|
# MegaLinter configuration - these override the action's defaults
|
||||||
APPLY_FIXES: none
|
|
||||||
APPLY_FIXES_EVENT: pull_request
|
|
||||||
APPLY_FIXES_MODE: commit
|
|
||||||
|
|
||||||
# Disable linters that do not work or conflict
|
|
||||||
DISABLE_LINTERS: REPOSITORY_DEVSKIM
|
DISABLE_LINTERS: REPOSITORY_DEVSKIM
|
||||||
|
|
||||||
# Additional settings
|
|
||||||
VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||||
GITHUB_TOKEN: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
# Report configuration
|
# Report configuration
|
||||||
REPORT_OUTPUT_FOLDER: megalinter-reports
|
REPORT_OUTPUT_FOLDER: megalinter-reports
|
||||||
@@ -72,35 +64,13 @@ jobs:
|
|||||||
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: MegaLinter
|
- name: Run MegaLinter
|
||||||
id: ml
|
id: pr-lint
|
||||||
uses: oxsecurity/megalinter/flavors/cupcake@62c799d895af9bcbca5eacfebca29d527f125a57 # v9.1.0
|
uses: ./pr-lint
|
||||||
|
|
||||||
- name: Check MegaLinter Results
|
|
||||||
id: check-results
|
|
||||||
if: always()
|
|
||||||
shell: sh
|
|
||||||
run: |
|
|
||||||
printf '%s\n' "status=success" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
if [ -f "${{ env.REPORT_OUTPUT_FOLDER }}/megalinter.log" ]; then
|
|
||||||
if grep -q "ERROR\|CRITICAL" "${{ env.REPORT_OUTPUT_FOLDER }}/megalinter.log"; then
|
|
||||||
echo "Linting errors found"
|
|
||||||
printf '%s\n' "status=failure" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "::warning::MegaLinter log file not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload Reports
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
with:
|
with:
|
||||||
name: MegaLinter reports
|
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
path: |
|
username: fiximus
|
||||||
megalinter-reports
|
email: github-bot@ivuorinen.net
|
||||||
mega-linter.log
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
- name: Upload SARIF Report
|
- name: Upload SARIF Report
|
||||||
if: always() && hashFiles('megalinter-reports/sarif/*.sarif')
|
if: always() && hashFiles('megalinter-reports/sarif/*.sarif')
|
||||||
@@ -109,74 +79,12 @@ jobs:
|
|||||||
sarif_file: megalinter-reports/sarif
|
sarif_file: megalinter-reports/sarif
|
||||||
category: megalinter
|
category: megalinter
|
||||||
|
|
||||||
- name: Prepare Git for Fixes
|
- name: Check Results
|
||||||
if: steps.ml.outputs.has_updated_sources == 1
|
|
||||||
shell: sh
|
|
||||||
run: |
|
|
||||||
sudo chown -Rc $(id -u) .git/
|
|
||||||
git config --global user.name "fiximus"
|
|
||||||
git config --global user.email "github-bot@ivuorinen.net"
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
if: |
|
|
||||||
steps.ml.outputs.has_updated_sources == 1 &&
|
|
||||||
(env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) &&
|
|
||||||
env.APPLY_FIXES_MODE == 'pull_request' &&
|
|
||||||
(github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) &&
|
|
||||||
!contains(github.event.head_commit.message, 'skip fix')
|
|
||||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
|
||||||
id: cpr
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
|
|
||||||
commit-message: '[MegaLinter] Apply linters automatic fixes'
|
|
||||||
title: '[MegaLinter] Apply linters automatic fixes'
|
|
||||||
labels: bot
|
|
||||||
branch: megalinter/fixes-${{ github.ref_name }}
|
|
||||||
branch-suffix: timestamp
|
|
||||||
delete-branch: true
|
|
||||||
body: |
|
|
||||||
## MegaLinter Fixes
|
|
||||||
|
|
||||||
MegaLinter has identified and fixed code style issues.
|
|
||||||
|
|
||||||
### 🔍 Changes Made
|
|
||||||
- Automated code style fixes
|
|
||||||
- Formatting improvements
|
|
||||||
- Lint error corrections
|
|
||||||
|
|
||||||
### 📝 Notes
|
|
||||||
- Please review the changes carefully
|
|
||||||
- Run tests before merging
|
|
||||||
- Verify formatting matches project standards
|
|
||||||
|
|
||||||
> Generated automatically by MegaLinter
|
|
||||||
|
|
||||||
- name: Commit Fixes
|
|
||||||
if: |
|
|
||||||
steps.ml.outputs.has_updated_sources == 1 &&
|
|
||||||
(env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) &&
|
|
||||||
env.APPLY_FIXES_MODE == 'commit' &&
|
|
||||||
github.ref != 'refs/heads/main' &&
|
|
||||||
(github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) &&
|
|
||||||
!contains(github.event.head_commit.message, 'skip fix')
|
|
||||||
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
|
|
||||||
branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
|
|
||||||
commit_message: |
|
|
||||||
style: apply MegaLinter fixes
|
|
||||||
|
|
||||||
[skip ci]
|
|
||||||
commit_user_name: fiximus
|
|
||||||
commit_user_email: github-bot@ivuorinen.net
|
|
||||||
push_options: --force
|
|
||||||
|
|
||||||
- name: Create Status Check
|
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const status = '${{ steps.check-results.outputs.status }}';
|
const status = '${{ steps.pr-lint.outputs.validation_status }}';
|
||||||
const conclusion = status === 'success' ? 'success' : 'failure';
|
const conclusion = status === 'success' ? 'success' : 'failure';
|
||||||
|
|
||||||
const summary = `## MegaLinter Results
|
const summary = `## MegaLinter Results
|
||||||
|
|||||||
2
.github/workflows/security-suite.yml
vendored
2
.github/workflows/security-suite.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
|||||||
# Record the base commit for diffing without checking it out
|
# Record the base commit for diffing without checking it out
|
||||||
# Keep PR head checked out so scanners analyze the new changes
|
# Keep PR head checked out so scanners analyze the new changes
|
||||||
BASE_REF="refs/remotes/origin-base/${{ github.event.pull_request.base.ref }}"
|
BASE_REF="refs/remotes/origin-base/${{ github.event.pull_request.base.ref }}"
|
||||||
echo "BASE_REF=${BASE_REF}" >> $GITHUB_ENV
|
echo "BASE_REF=${BASE_REF}" >> "$GITHUB_ENV"
|
||||||
echo "Base ref: ${BASE_REF}"
|
echo "Base ref: ${BASE_REF}"
|
||||||
git log -1 --oneline "${BASE_REF}"
|
git log -1 --oneline "${BASE_REF}"
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/test-actions.yml
vendored
4
.github/workflows/test-actions.yml
vendored
@@ -125,10 +125,10 @@ jobs:
|
|||||||
shell: sh
|
shell: sh
|
||||||
run: |
|
run: |
|
||||||
if [ -d "_tests/reports/integration" ] && [ -n "$(find _tests/reports/integration -type f 2>/dev/null)" ]; then
|
if [ -d "_tests/reports/integration" ] && [ -n "$(find _tests/reports/integration -type f 2>/dev/null)" ]; then
|
||||||
printf '%s\n' "reports-found=true" >> $GITHUB_OUTPUT
|
printf '%s\n' "reports-found=true" >> "$GITHUB_OUTPUT"
|
||||||
echo "Integration test reports found"
|
echo "Integration test reports found"
|
||||||
else
|
else
|
||||||
printf '%s\n' "reports-found=false" >> $GITHUB_OUTPUT
|
printf '%s\n' "reports-found=false" >> "$GITHUB_OUTPUT"
|
||||||
echo "No integration test reports found"
|
echo "No integration test reports found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
- **Path**: /Users/ivuorinen/Code/ivuorinen/actions
|
- **Path**: /Users/ivuorinen/Code/ivuorinen/actions
|
||||||
- **Branch**: main
|
- **Branch**: main
|
||||||
- **External Usage**: `ivuorinen/actions/<action-name>@main`
|
- **External Usage**: `ivuorinen/actions/<action-name>@main`
|
||||||
- **Total Actions**: 43 self-contained actions
|
- **Total Actions**: 44 self-contained actions
|
||||||
|
- **Dogfooding**: Workflows use local actions (pr-lint, codeql-analysis, security-scan)
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/
|
/
|
||||||
├── <action-dirs>/ # 43 self-contained actions
|
├── <action-dirs>/ # 44 self-contained actions
|
||||||
│ ├── action.yml # Action definition
|
│ ├── action.yml # Action definition
|
||||||
│ ├── README.md # Auto-generated
|
│ ├── README.md # Auto-generated
|
||||||
│ └── CustomValidator.py # Optional validator
|
│ └── CustomValidator.py # Optional validator
|
||||||
@@ -25,12 +26,14 @@
|
|||||||
└── Makefile # Build automation
|
└── Makefile # Build automation
|
||||||
```
|
```
|
||||||
|
|
||||||
## Action Categories (43 total)
|
## Action Categories (44 total)
|
||||||
|
|
||||||
**Setup (7)**: node-setup, set-git-config, php-version-detect, python-version-detect, python-version-detect-v2, go-version-detect, dotnet-version-detect
|
**Setup (7)**: node-setup, set-git-config, php-version-detect, python-version-detect, python-version-detect-v2, go-version-detect, dotnet-version-detect
|
||||||
|
|
||||||
**Linting (13)**: ansible-lint-fix, biome-check/fix, csharp-lint-check, eslint-check/fix, go-lint, pr-lint, pre-commit, prettier-check/fix, python-lint-fix, terraform-lint-fix
|
**Linting (13)**: ansible-lint-fix, biome-check/fix, csharp-lint-check, eslint-check/fix, go-lint, pr-lint, pre-commit, prettier-check/fix, python-lint-fix, terraform-lint-fix
|
||||||
|
|
||||||
|
**Security (1)**: security-scan (actionlint, Gitleaks, Trivy scanning)
|
||||||
|
|
||||||
**Build (3)**: csharp-build, go-build, docker-build
|
**Build (3)**: csharp-build, go-build, docker-build
|
||||||
|
|
||||||
**Publishing (5)**: npm-publish, docker-publish, docker-publish-gh, docker-publish-hub, csharp-publish
|
**Publishing (5)**: npm-publish, docker-publish, docker-publish-gh, docker-publish-hub, csharp-publish
|
||||||
@@ -85,3 +88,28 @@ make test # All tests (pytest + ShellSpec)
|
|||||||
- ✅ Convention-based validation
|
- ✅ Convention-based validation
|
||||||
- ✅ Test generation system
|
- ✅ Test generation system
|
||||||
- ✅ Full backward compatibility
|
- ✅ Full backward compatibility
|
||||||
|
|
||||||
|
## Dogfooding Strategy
|
||||||
|
|
||||||
|
The repository actively dogfoods its own actions in workflows:
|
||||||
|
|
||||||
|
**Fully Dogfooded Workflows**:
|
||||||
|
|
||||||
|
- **pr-lint.yml**: Uses `./pr-lint` (was 204 lines, now 112 lines - 45% reduction)
|
||||||
|
- **action-security.yml**: Uses `./security-scan` (was 264 lines, now 82 lines - 69% reduction)
|
||||||
|
- **codeql-new.yml**: Uses `./codeql-analysis`
|
||||||
|
- **sync-labels.yml**: Uses `./sync-labels`
|
||||||
|
- **version-maintenance.yml**: Uses `./action-versioning`
|
||||||
|
|
||||||
|
**Intentionally External**:
|
||||||
|
|
||||||
|
- **build-testing-image.yml**: Uses docker/\* actions directly (needs metadata extraction)
|
||||||
|
- Core GitHub actions (checkout, upload-artifact, setup-\*) kept for standardization
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
|
||||||
|
- Early detection of action issues
|
||||||
|
- Real-world testing of actions
|
||||||
|
- Reduced workflow duplication
|
||||||
|
- Improved maintainability
|
||||||
|
- Better documentation through usage examples
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -22,9 +22,9 @@ Each action is fully self-contained and can be used independently in any GitHub
|
|||||||
|
|
||||||
## 📚 Action Catalog
|
## 📚 Action Catalog
|
||||||
|
|
||||||
This repository contains **25 reusable GitHub Actions** for CI/CD automation.
|
This repository contains **26 reusable GitHub Actions** for CI/CD automation.
|
||||||
|
|
||||||
### Quick Reference (25 Actions)
|
### Quick Reference (26 Actions)
|
||||||
|
|
||||||
| Icon | Action | Category | Description | Key Features |
|
| Icon | Action | Category | Description | Key Features |
|
||||||
|:----:|:-----------------------------------------------------|:-----------|:----------------------------------------------------------------|:---------------------------------------------|
|
|:----:|:-----------------------------------------------------|:-----------|:----------------------------------------------------------------|:---------------------------------------------|
|
||||||
@@ -34,7 +34,7 @@ This repository contains **25 reusable GitHub Actions** for CI/CD automation.
|
|||||||
| 🛡️ | [`codeql-analysis`][codeql-analysis] | Repository | Run CodeQL security analysis for a single language with conf... | Auto-detection, Token auth, Outputs |
|
| 🛡️ | [`codeql-analysis`][codeql-analysis] | Repository | Run CodeQL security analysis for a single language with conf... | Auto-detection, Token auth, Outputs |
|
||||||
| 🖼️ | [`compress-images`][compress-images] | Repository | Compress images on demand (workflow_dispatch), and at 11pm e... | Token auth, Outputs |
|
| 🖼️ | [`compress-images`][compress-images] | Repository | Compress images on demand (workflow_dispatch), and at 11pm e... | Token auth, Outputs |
|
||||||
| 📝 | [`csharp-build`][csharp-build] | Build | Builds and tests C# projects. | Caching, Auto-detection, Token auth, Outputs |
|
| 📝 | [`csharp-build`][csharp-build] | Build | Builds and tests C# projects. | Caching, Auto-detection, Token auth, Outputs |
|
||||||
| 📝 | [`csharp-lint-check`][csharp-lint-check] | Linting | Runs linters like StyleCop or dotnet-format for C# code styl... | Auto-detection, Token auth, Outputs |
|
| 📝 | [`csharp-lint-check`][csharp-lint-check] | Linting | Runs linters like StyleCop or dotnet-format for C# code styl... | Caching, Auto-detection, Token auth, Outputs |
|
||||||
| 📦 | [`csharp-publish`][csharp-publish] | Publishing | Publishes a C# project to GitHub Packages. | Caching, Auto-detection, Token auth, Outputs |
|
| 📦 | [`csharp-publish`][csharp-publish] | Publishing | Publishes a C# project to GitHub Packages. | Caching, Auto-detection, Token auth, Outputs |
|
||||||
| 📦 | [`docker-build`][docker-build] | Build | Builds a Docker image for multiple architectures with enhanc... | Caching, Auto-detection, Token auth, Outputs |
|
| 📦 | [`docker-build`][docker-build] | Build | Builds a Docker image for multiple architectures with enhanc... | Caching, Auto-detection, Token auth, Outputs |
|
||||||
| ☁️ | [`docker-publish`][docker-publish] | Publishing | Simple wrapper to publish Docker images to GitHub Packages a... | Token auth, Outputs |
|
| ☁️ | [`docker-publish`][docker-publish] | Publishing | Simple wrapper to publish Docker images to GitHub Packages a... | Token auth, Outputs |
|
||||||
@@ -49,6 +49,7 @@ This repository contains **25 reusable GitHub Actions** for CI/CD automation.
|
|||||||
| ✅ | [`prettier-lint`][prettier-lint] | Linting | Run Prettier in check or fix mode with advanced configuratio... | Caching, Auto-detection, Token auth, Outputs |
|
| ✅ | [`prettier-lint`][prettier-lint] | Linting | Run Prettier in check or fix mode with advanced configuratio... | Caching, Auto-detection, Token auth, Outputs |
|
||||||
| 📝 | [`python-lint-fix`][python-lint-fix] | Linting | Lints and fixes Python files, commits changes, and uploads S... | Caching, Auto-detection, Token auth, Outputs |
|
| 📝 | [`python-lint-fix`][python-lint-fix] | Linting | Lints and fixes Python files, commits changes, and uploads S... | Caching, Auto-detection, Token auth, Outputs |
|
||||||
| 📦 | [`release-monthly`][release-monthly] | Repository | Creates a release for the current month, incrementing patch ... | Token auth, Outputs |
|
| 📦 | [`release-monthly`][release-monthly] | Repository | Creates a release for the current month, incrementing patch ... | Token auth, Outputs |
|
||||||
|
| 🛡️ | [`security-scan`][security-scan] | Security | Comprehensive security scanning for GitHub Actions including... | Caching, Token auth, Outputs |
|
||||||
| 📦 | [`stale`][stale] | Repository | A GitHub Action to close stale issues and pull requests. | Token auth, Outputs |
|
| 📦 | [`stale`][stale] | Repository | A GitHub Action to close stale issues and pull requests. | Token auth, Outputs |
|
||||||
| 🏷️ | [`sync-labels`][sync-labels] | Repository | Sync labels from a YAML file to a GitHub repository | Token auth, Outputs |
|
| 🏷️ | [`sync-labels`][sync-labels] | Repository | Sync labels from a YAML file to a GitHub repository | Token auth, Outputs |
|
||||||
| 🖥️ | [`terraform-lint-fix`][terraform-lint-fix] | Linting | Lints and fixes Terraform files with advanced validation and... | Token auth, Outputs |
|
| 🖥️ | [`terraform-lint-fix`][terraform-lint-fix] | Linting | Lints and fixes Terraform files with advanced validation and... | Token auth, Outputs |
|
||||||
@@ -74,7 +75,7 @@ This repository contains **25 reusable GitHub Actions** for CI/CD automation.
|
|||||||
|:-----------------------------------------------|:------------------------------------------------------|:---------------------------------------------|:---------------------------------------------|
|
|:-----------------------------------------------|:------------------------------------------------------|:---------------------------------------------|:---------------------------------------------|
|
||||||
| 📦 [`ansible-lint-fix`][ansible-lint-fix] | Lints and fixes Ansible playbooks, commits changes... | Ansible, YAML | Caching, Token auth, Outputs |
|
| 📦 [`ansible-lint-fix`][ansible-lint-fix] | Lints and fixes Ansible playbooks, commits changes... | Ansible, YAML | Caching, Token auth, Outputs |
|
||||||
| ✅ [`biome-lint`][biome-lint] | Run Biome linter in check or fix mode | JavaScript, TypeScript, JSON | Caching, Auto-detection, Token auth, Outputs |
|
| ✅ [`biome-lint`][biome-lint] | Run Biome linter in check or fix mode | JavaScript, TypeScript, JSON | Caching, Auto-detection, Token auth, Outputs |
|
||||||
| 📝 [`csharp-lint-check`][csharp-lint-check] | Runs linters like StyleCop or dotnet-format for C#... | C#, .NET | Auto-detection, Token auth, Outputs |
|
| 📝 [`csharp-lint-check`][csharp-lint-check] | Runs linters like StyleCop or dotnet-format for C#... | C#, .NET | Caching, Auto-detection, Token auth, Outputs |
|
||||||
| ✅ [`eslint-lint`][eslint-lint] | Run ESLint in check or fix mode with advanced conf... | JavaScript, TypeScript | Caching, Auto-detection, Token auth, Outputs |
|
| ✅ [`eslint-lint`][eslint-lint] | Run ESLint in check or fix mode with advanced conf... | JavaScript, TypeScript | Caching, Auto-detection, Token auth, Outputs |
|
||||||
| 📝 [`go-lint`][go-lint] | Run golangci-lint with advanced configuration, cac... | Go | Caching, Token auth, Outputs |
|
| 📝 [`go-lint`][go-lint] | Run golangci-lint with advanced configuration, cac... | Go | Caching, Token auth, Outputs |
|
||||||
| ✅ [`pr-lint`][pr-lint] | Runs MegaLinter against pull requests | Conventional Commits | Caching, Auto-detection, Token auth, Outputs |
|
| ✅ [`pr-lint`][pr-lint] | Runs MegaLinter against pull requests | Conventional Commits | Caching, Auto-detection, Token auth, Outputs |
|
||||||
@@ -115,6 +116,12 @@ This repository contains **25 reusable GitHub Actions** for CI/CD automation.
|
|||||||
| 📦 [`stale`][stale] | A GitHub Action to close stale issues and pull req... | GitHub Actions | Token auth, Outputs |
|
| 📦 [`stale`][stale] | A GitHub Action to close stale issues and pull req... | GitHub Actions | Token auth, Outputs |
|
||||||
| 🏷️ [`sync-labels`][sync-labels] | Sync labels from a YAML file to a GitHub repositor... | YAML, GitHub | Token auth, Outputs |
|
| 🏷️ [`sync-labels`][sync-labels] | Sync labels from a YAML file to a GitHub repositor... | YAML, GitHub | Token auth, Outputs |
|
||||||
|
|
||||||
|
#### 🛡️ Security (1 action)
|
||||||
|
|
||||||
|
| Action | Description | Languages | Features |
|
||||||
|
|:-------------------------------------|:------------------------------------------------------|:----------|:-----------------------------|
|
||||||
|
| 🛡️ [`security-scan`][security-scan] | Comprehensive security scanning for GitHub Actions... | - | Caching, Token auth, Outputs |
|
||||||
|
|
||||||
#### ✅ Validation (1 action)
|
#### ✅ Validation (1 action)
|
||||||
|
|
||||||
| Action | Description | Languages | Features |
|
| Action | Description | Languages | Features |
|
||||||
@@ -131,7 +138,7 @@ This repository contains **25 reusable GitHub Actions** for CI/CD automation.
|
|||||||
| [`codeql-analysis`][codeql-analysis] | - | ✅ | ✅ | ✅ |
|
| [`codeql-analysis`][codeql-analysis] | - | ✅ | ✅ | ✅ |
|
||||||
| [`compress-images`][compress-images] | - | - | ✅ | ✅ |
|
| [`compress-images`][compress-images] | - | - | ✅ | ✅ |
|
||||||
| [`csharp-build`][csharp-build] | ✅ | ✅ | ✅ | ✅ |
|
| [`csharp-build`][csharp-build] | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [`csharp-lint-check`][csharp-lint-check] | - | ✅ | ✅ | ✅ |
|
| [`csharp-lint-check`][csharp-lint-check] | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [`csharp-publish`][csharp-publish] | ✅ | ✅ | ✅ | ✅ |
|
| [`csharp-publish`][csharp-publish] | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [`docker-build`][docker-build] | ✅ | ✅ | ✅ | ✅ |
|
| [`docker-build`][docker-build] | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [`docker-publish`][docker-publish] | - | - | ✅ | ✅ |
|
| [`docker-publish`][docker-publish] | - | - | ✅ | ✅ |
|
||||||
@@ -146,6 +153,7 @@ This repository contains **25 reusable GitHub Actions** for CI/CD automation.
|
|||||||
| [`prettier-lint`][prettier-lint] | ✅ | ✅ | ✅ | ✅ |
|
| [`prettier-lint`][prettier-lint] | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [`python-lint-fix`][python-lint-fix] | ✅ | ✅ | ✅ | ✅ |
|
| [`python-lint-fix`][python-lint-fix] | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [`release-monthly`][release-monthly] | - | - | ✅ | ✅ |
|
| [`release-monthly`][release-monthly] | - | - | ✅ | ✅ |
|
||||||
|
| [`security-scan`][security-scan] | ✅ | - | ✅ | ✅ |
|
||||||
| [`stale`][stale] | - | - | ✅ | ✅ |
|
| [`stale`][stale] | - | - | ✅ | ✅ |
|
||||||
| [`sync-labels`][sync-labels] | - | - | ✅ | ✅ |
|
| [`sync-labels`][sync-labels] | - | - | ✅ | ✅ |
|
||||||
| [`terraform-lint-fix`][terraform-lint-fix] | - | - | ✅ | ✅ |
|
| [`terraform-lint-fix`][terraform-lint-fix] | - | - | ✅ | ✅ |
|
||||||
@@ -224,6 +232,7 @@ All actions can be used independently in your workflows:
|
|||||||
[prettier-lint]: prettier-lint/README.md
|
[prettier-lint]: prettier-lint/README.md
|
||||||
[python-lint-fix]: python-lint-fix/README.md
|
[python-lint-fix]: python-lint-fix/README.md
|
||||||
[release-monthly]: release-monthly/README.md
|
[release-monthly]: release-monthly/README.md
|
||||||
|
[security-scan]: security-scan/README.md
|
||||||
[stale]: stale/README.md
|
[stale]: stale/README.md
|
||||||
[sync-labels]: sync-labels/README.md
|
[sync-labels]: sync-labels/README.md
|
||||||
[terraform-lint-fix]: terraform-lint-fix/README.md
|
[terraform-lint-fix]: terraform-lint-fix/README.md
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ set -euo pipefail
|
|||||||
|
|
||||||
# Source setup utilities
|
# Source setup utilities
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
||||||
# shellcheck source=_tests/framework/setup.sh
|
# shellcheck source=_tests/framework/setup.sh
|
||||||
|
# shellcheck disable=SC1091
|
||||||
source "${SCRIPT_DIR}/setup.sh"
|
source "${SCRIPT_DIR}/setup.sh"
|
||||||
|
|
||||||
# Action testing utilities
|
# Action testing utilities
|
||||||
@@ -57,6 +57,13 @@ get_action_name() {
|
|||||||
uv run "$script_dir/../shared/validation_core.py" --name "$action_file"
|
uv run "$script_dir/../shared/validation_core.py" --name "$action_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_action_runs_using() {
|
||||||
|
local action_file="$1"
|
||||||
|
local script_dir
|
||||||
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
uv run "$script_dir/../shared/validation_core.py" --runs-using "$action_file"
|
||||||
|
}
|
||||||
|
|
||||||
# Check if an input is required in an action.yml file
|
# Check if an input is required in an action.yml file
|
||||||
is_input_required() {
|
is_input_required() {
|
||||||
local action_file="$1"
|
local action_file="$1"
|
||||||
@@ -69,7 +76,7 @@ is_input_required() {
|
|||||||
required_status=$(uv run "$script_dir/../shared/validation_core.py" --property "$action_file" "$input_name" "required")
|
required_status=$(uv run "$script_dir/../shared/validation_core.py" --property "$action_file" "$input_name" "required")
|
||||||
|
|
||||||
# Return 0 (success) if input is required, 1 (failure) if optional
|
# Return 0 (success) if input is required, 1 (failure) if optional
|
||||||
[[ $required_status == "required" ]]
|
[[ "$required_status" == "required" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test input validation using Python validation module
|
# Test input validation using Python validation module
|
||||||
@@ -363,5 +370,5 @@ run_action_tests() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Export all functions
|
# Export all functions
|
||||||
export -f validate_action_yml get_action_inputs get_action_outputs get_action_name is_input_required
|
export -f validate_action_yml get_action_inputs get_action_outputs get_action_name get_action_runs_using is_input_required
|
||||||
export -f test_input_validation test_action_outputs test_external_usage measure_action_time run_action_tests
|
export -f test_input_validation test_action_outputs test_external_usage measure_action_time run_action_tests
|
||||||
|
|||||||
@@ -521,6 +521,16 @@ class ActionFileParser:
|
|||||||
except (OSError, ValueError, yaml.YAMLError, AttributeError):
|
except (OSError, ValueError, yaml.YAMLError, AttributeError):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_action_runs_using(action_file: str) -> str:
|
||||||
|
"""Get the runs.using value from an action.yml file."""
|
||||||
|
try:
|
||||||
|
data = ActionFileParser.load_action_file(action_file)
|
||||||
|
runs = data.get("runs", {})
|
||||||
|
return runs.get("using", "unknown")
|
||||||
|
except (OSError, ValueError, yaml.YAMLError, AttributeError):
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_required_property(input_data: dict, property_name: str) -> str:
|
def _get_required_property(input_data: dict, property_name: str) -> str:
|
||||||
"""Get the required/optional property."""
|
"""Get the required/optional property."""
|
||||||
@@ -787,6 +797,11 @@ Examples:
|
|||||||
mode_group.add_argument("--inputs", metavar="ACTION_FILE", help="List action inputs")
|
mode_group.add_argument("--inputs", metavar="ACTION_FILE", help="List action inputs")
|
||||||
mode_group.add_argument("--outputs", metavar="ACTION_FILE", help="List action outputs")
|
mode_group.add_argument("--outputs", metavar="ACTION_FILE", help="List action outputs")
|
||||||
mode_group.add_argument("--name", metavar="ACTION_FILE", help="Get action name")
|
mode_group.add_argument("--name", metavar="ACTION_FILE", help="Get action name")
|
||||||
|
mode_group.add_argument(
|
||||||
|
"--runs-using",
|
||||||
|
metavar="ACTION_FILE",
|
||||||
|
help="Get action runs.using value",
|
||||||
|
)
|
||||||
mode_group.add_argument(
|
mode_group.add_argument(
|
||||||
"--validate-yaml",
|
"--validate-yaml",
|
||||||
metavar="YAML_FILE",
|
metavar="YAML_FILE",
|
||||||
@@ -834,6 +849,12 @@ def _handle_name_command(args):
|
|||||||
print(name)
|
print(name)
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_runs_using_command(args):
|
||||||
|
"""Handle the runs-using command."""
|
||||||
|
runs_using = ActionFileParser.get_action_runs_using(args.runs_using)
|
||||||
|
print(runs_using)
|
||||||
|
|
||||||
|
|
||||||
def _handle_validate_yaml_command(args):
|
def _handle_validate_yaml_command(args):
|
||||||
"""Handle the validate-yaml command."""
|
"""Handle the validate-yaml command."""
|
||||||
try:
|
try:
|
||||||
@@ -853,6 +874,7 @@ def _execute_command(args):
|
|||||||
"inputs": _handle_inputs_command,
|
"inputs": _handle_inputs_command,
|
||||||
"outputs": _handle_outputs_command,
|
"outputs": _handle_outputs_command,
|
||||||
"name": _handle_name_command,
|
"name": _handle_name_command,
|
||||||
|
"runs_using": _handle_runs_using_command,
|
||||||
"validate_yaml": _handle_validate_yaml_command,
|
"validate_yaml": _handle_validate_yaml_command,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
116
_tests/unit/security-scan/validation.spec.sh
Executable file
116
_tests/unit/security-scan/validation.spec.sh
Executable file
@@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env shellspec
|
||||||
|
# Unit tests for security-scan action validation and logic
|
||||||
|
# Framework is automatically loaded via spec_helper.sh
|
||||||
|
|
||||||
|
Describe "security-scan action"
|
||||||
|
ACTION_DIR="security-scan"
|
||||||
|
ACTION_FILE="$ACTION_DIR/action.yml"
|
||||||
|
|
||||||
|
Context "when validating token input"
|
||||||
|
It "accepts valid GitHub token"
|
||||||
|
When call validate_input_python "security-scan" "token" "ghp_123456789012345678901234567890123456"
|
||||||
|
The status should be success
|
||||||
|
End
|
||||||
|
|
||||||
|
It "rejects injection in token"
|
||||||
|
When call validate_input_python "security-scan" "token" "token; rm -rf /"
|
||||||
|
The status should be failure
|
||||||
|
End
|
||||||
|
|
||||||
|
It "accepts empty token (optional)"
|
||||||
|
When call validate_input_python "security-scan" "token" ""
|
||||||
|
The status should be success
|
||||||
|
End
|
||||||
|
End
|
||||||
|
|
||||||
|
Context "when validating actionlint-enabled input"
|
||||||
|
It "accepts true value"
|
||||||
|
When call validate_input_python "security-scan" "actionlint-enabled" "true"
|
||||||
|
The status should be success
|
||||||
|
End
|
||||||
|
|
||||||
|
It "accepts false value"
|
||||||
|
When call validate_input_python "security-scan" "actionlint-enabled" "false"
|
||||||
|
The status should be success
|
||||||
|
End
|
||||||
|
|
||||||
|
It "rejects non-boolean value"
|
||||||
|
When call validate_input_python "security-scan" "actionlint-enabled" "maybe"
|
||||||
|
The status should be failure
|
||||||
|
End
|
||||||
|
End
|
||||||
|
|
||||||
|
Context "when checking action.yml structure"
|
||||||
|
It "has valid YAML syntax"
|
||||||
|
When call validate_action_yml_quiet "$ACTION_FILE"
|
||||||
|
The status should be success
|
||||||
|
End
|
||||||
|
|
||||||
|
It "has correct action name"
|
||||||
|
name=$(get_action_name "$ACTION_FILE")
|
||||||
|
When call echo "$name"
|
||||||
|
The output should equal "Security Scan"
|
||||||
|
End
|
||||||
|
|
||||||
|
It "defines all expected inputs"
|
||||||
|
inputs=$(get_action_inputs "$ACTION_FILE")
|
||||||
|
When call echo "$inputs"
|
||||||
|
The output should include "gitleaks-license"
|
||||||
|
The output should include "gitleaks-config"
|
||||||
|
The output should include "trivy-severity"
|
||||||
|
The output should include "trivy-scanners"
|
||||||
|
The output should include "trivy-timeout"
|
||||||
|
The output should include "actionlint-enabled"
|
||||||
|
The output should include "token"
|
||||||
|
End
|
||||||
|
|
||||||
|
It "defines all expected outputs"
|
||||||
|
outputs=$(get_action_outputs "$ACTION_FILE")
|
||||||
|
When call echo "$outputs"
|
||||||
|
The output should include "has_trivy_results"
|
||||||
|
The output should include "has_gitleaks_results"
|
||||||
|
The output should include "total_issues"
|
||||||
|
The output should include "critical_issues"
|
||||||
|
End
|
||||||
|
|
||||||
|
It "uses composite run type"
|
||||||
|
run_type=$(get_action_runs_using "$ACTION_FILE")
|
||||||
|
When call echo "$run_type"
|
||||||
|
The output should equal "composite"
|
||||||
|
End
|
||||||
|
End
|
||||||
|
|
||||||
|
Context "when validating inputs per conventions"
|
||||||
|
It "validates token against github_token convention"
|
||||||
|
When call validate_input_python "security-scan" "token" "ghp_123456789012345678901234567890123456"
|
||||||
|
The status should be success
|
||||||
|
End
|
||||||
|
|
||||||
|
It "validates actionlint-enabled as boolean"
|
||||||
|
When call validate_input_python "security-scan" "actionlint-enabled" "true"
|
||||||
|
The status should be success
|
||||||
|
End
|
||||||
|
|
||||||
|
It "rejects invalid boolean for actionlint-enabled"
|
||||||
|
When call validate_input_python "security-scan" "actionlint-enabled" "1"
|
||||||
|
The status should be failure
|
||||||
|
End
|
||||||
|
End
|
||||||
|
|
||||||
|
Context "when testing optional inputs"
|
||||||
|
It "accepts empty gitleaks-license"
|
||||||
|
When call validate_input_python "security-scan" "gitleaks-license" ""
|
||||||
|
The status should be success
|
||||||
|
End
|
||||||
|
|
||||||
|
It "accepts empty token"
|
||||||
|
When call validate_input_python "security-scan" "token" ""
|
||||||
|
The status should be success
|
||||||
|
End
|
||||||
|
|
||||||
|
It "accepts valid gitleaks-license value"
|
||||||
|
When call validate_input_python "security-scan" "gitleaks-license" "license-key-123"
|
||||||
|
The status should be success
|
||||||
|
End
|
||||||
|
End
|
||||||
|
End
|
||||||
@@ -28,7 +28,8 @@ conventions:
|
|||||||
mode: mode_enum
|
mode: mode_enum
|
||||||
token: github_token
|
token: github_token
|
||||||
username: username
|
username: username
|
||||||
overrides: {}
|
overrides:
|
||||||
|
mode: mode_enum
|
||||||
statistics:
|
statistics:
|
||||||
total_inputs: 6
|
total_inputs: 6
|
||||||
validated_inputs: 6
|
validated_inputs: 6
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ conventions:
|
|||||||
packs: codeql_packs
|
packs: codeql_packs
|
||||||
queries: codeql_queries
|
queries: codeql_queries
|
||||||
ram: numeric_range_256_32768
|
ram: numeric_range_256_32768
|
||||||
skip-queries: codeql_queries
|
skip-queries: boolean
|
||||||
source-root: file_path
|
source-root: file_path
|
||||||
threads: numeric_range_1_128
|
threads: numeric_range_1_128
|
||||||
token: github_token
|
token: github_token
|
||||||
@@ -51,6 +51,7 @@ overrides:
|
|||||||
build-mode: codeql_build_mode
|
build-mode: codeql_build_mode
|
||||||
category: category_format
|
category: category_format
|
||||||
config: codeql_config
|
config: codeql_config
|
||||||
|
language: codeql_language
|
||||||
output: file_path
|
output: file_path
|
||||||
packs: codeql_packs
|
packs: codeql_packs
|
||||||
queries: codeql_queries
|
queries: codeql_queries
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Validation rules for compress-images action
|
# Validation rules for compress-images action
|
||||||
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
||||||
# Schema version: 1.0
|
# Schema version: 1.0
|
||||||
# Coverage: 86% (6/7 inputs)
|
# Coverage: 100% (7/7 inputs)
|
||||||
#
|
#
|
||||||
# This file defines validation rules for the compress-images GitHub Action.
|
# This file defines validation rules for the compress-images GitHub Action.
|
||||||
# Rules are automatically applied by validate-inputs action when this
|
# Rules are automatically applied by validate-inputs action when this
|
||||||
@@ -24,6 +24,7 @@ optional_inputs:
|
|||||||
- working-directory
|
- working-directory
|
||||||
conventions:
|
conventions:
|
||||||
email: email
|
email: email
|
||||||
|
ignore-paths: path_list
|
||||||
image-quality: numeric_range_0_100
|
image-quality: numeric_range_0_100
|
||||||
png-quality: numeric_range_0_100
|
png-quality: numeric_range_0_100
|
||||||
token: github_token
|
token: github_token
|
||||||
@@ -32,10 +33,10 @@ conventions:
|
|||||||
overrides: {}
|
overrides: {}
|
||||||
statistics:
|
statistics:
|
||||||
total_inputs: 7
|
total_inputs: 7
|
||||||
validated_inputs: 6
|
validated_inputs: 7
|
||||||
skipped_inputs: 0
|
skipped_inputs: 0
|
||||||
coverage_percentage: 86
|
coverage_percentage: 100
|
||||||
validation_coverage: 86
|
validation_coverage: 100
|
||||||
auto_detected: true
|
auto_detected: true
|
||||||
manual_review_required: false
|
manual_review_required: false
|
||||||
quality_indicators:
|
quality_indicators:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Validation rules for docker-build action
|
# Validation rules for docker-build action
|
||||||
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
||||||
# Schema version: 1.0
|
# Schema version: 1.0
|
||||||
# Coverage: 63% (17/27 inputs)
|
# Coverage: 100% (27/27 inputs)
|
||||||
#
|
#
|
||||||
# This file defines validation rules for the docker-build GitHub Action.
|
# This file defines validation rules for the docker-build GitHub Action.
|
||||||
# Rules are automatically applied by validate-inputs action when this
|
# Rules are automatically applied by validate-inputs action when this
|
||||||
@@ -45,17 +45,27 @@ optional_inputs:
|
|||||||
conventions:
|
conventions:
|
||||||
architectures: docker_architectures
|
architectures: docker_architectures
|
||||||
auto-detect-platforms: docker_architectures
|
auto-detect-platforms: docker_architectures
|
||||||
|
build-args: key_value_list
|
||||||
|
build-contexts: key_value_list
|
||||||
buildkit-version: semantic_version
|
buildkit-version: semantic_version
|
||||||
buildx-version: semantic_version
|
buildx-version: semantic_version
|
||||||
cache-mode: boolean
|
cache-export: cache_config
|
||||||
|
cache-from: cache_config
|
||||||
|
cache-import: cache_config
|
||||||
|
cache-mode: cache_mode
|
||||||
|
context: file_path
|
||||||
dockerfile: file_path
|
dockerfile: file_path
|
||||||
dry-run: boolean
|
dry-run: boolean
|
||||||
image-name: docker_image_name
|
image-name: docker_image_name
|
||||||
max-retries: numeric_range_1_10
|
max-retries: numeric_range_1_10
|
||||||
|
network: network_mode
|
||||||
parallel-builds: numeric_range_0_16
|
parallel-builds: numeric_range_0_16
|
||||||
|
platform-build-args: json_format
|
||||||
platform-fallback: docker_architectures
|
platform-fallback: docker_architectures
|
||||||
sbom-format: report_format
|
push: boolean
|
||||||
|
sbom-format: sbom_format
|
||||||
scan-image: boolean
|
scan-image: boolean
|
||||||
|
secrets: key_value_list
|
||||||
sign-image: boolean
|
sign-image: boolean
|
||||||
tag: docker_tag
|
tag: docker_tag
|
||||||
token: github_token
|
token: github_token
|
||||||
@@ -65,12 +75,12 @@ overrides:
|
|||||||
sbom-format: sbom_format
|
sbom-format: sbom_format
|
||||||
statistics:
|
statistics:
|
||||||
total_inputs: 27
|
total_inputs: 27
|
||||||
validated_inputs: 17
|
validated_inputs: 27
|
||||||
skipped_inputs: 0
|
skipped_inputs: 0
|
||||||
coverage_percentage: 63
|
coverage_percentage: 100
|
||||||
validation_coverage: 63
|
validation_coverage: 100
|
||||||
auto_detected: true
|
auto_detected: true
|
||||||
manual_review_required: true
|
manual_review_required: false
|
||||||
quality_indicators:
|
quality_indicators:
|
||||||
has_required_inputs: true
|
has_required_inputs: true
|
||||||
has_token_validation: true
|
has_token_validation: true
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Validation rules for docker-publish action
|
# Validation rules for docker-publish action
|
||||||
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
||||||
# Schema version: 1.0
|
# Schema version: 1.0
|
||||||
# Coverage: 73% (8/11 inputs)
|
# Coverage: 100% (11/11 inputs)
|
||||||
#
|
#
|
||||||
# This file defines validation rules for the docker-publish GitHub Action.
|
# This file defines validation rules for the docker-publish GitHub Action.
|
||||||
# Rules are automatically applied by validate-inputs action when this
|
# Rules are automatically applied by validate-inputs action when this
|
||||||
@@ -27,25 +27,27 @@ optional_inputs:
|
|||||||
- tags
|
- tags
|
||||||
- token
|
- token
|
||||||
conventions:
|
conventions:
|
||||||
|
build-args: key_value_list
|
||||||
|
context: file_path
|
||||||
dockerfile: file_path
|
dockerfile: file_path
|
||||||
dockerhub-token: github_token
|
dockerhub-token: github_token
|
||||||
dockerhub-username: username
|
dockerhub-username: username
|
||||||
image-name: docker_image_name
|
image-name: docker_image_name
|
||||||
platforms: docker_architectures
|
platforms: docker_architectures
|
||||||
registry: registry
|
push: boolean
|
||||||
|
registry: registry_enum
|
||||||
tags: docker_tag
|
tags: docker_tag
|
||||||
token: github_token
|
token: github_token
|
||||||
overrides:
|
overrides:
|
||||||
platforms: null
|
|
||||||
registry: registry_enum
|
registry: registry_enum
|
||||||
statistics:
|
statistics:
|
||||||
total_inputs: 11
|
total_inputs: 11
|
||||||
validated_inputs: 8
|
validated_inputs: 11
|
||||||
skipped_inputs: 1
|
skipped_inputs: 0
|
||||||
coverage_percentage: 73
|
coverage_percentage: 100
|
||||||
validation_coverage: 73
|
validation_coverage: 100
|
||||||
auto_detected: true
|
auto_detected: true
|
||||||
manual_review_required: true
|
manual_review_required: false
|
||||||
quality_indicators:
|
quality_indicators:
|
||||||
has_required_inputs: false
|
has_required_inputs: false
|
||||||
has_token_validation: true
|
has_token_validation: true
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ conventions:
|
|||||||
token: github_token
|
token: github_token
|
||||||
username: username
|
username: username
|
||||||
working-directory: file_path
|
working-directory: file_path
|
||||||
overrides: {}
|
overrides:
|
||||||
|
mode: mode_enum
|
||||||
statistics:
|
statistics:
|
||||||
total_inputs: 14
|
total_inputs: 14
|
||||||
validated_inputs: 14
|
validated_inputs: 14
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ const CATEGORIES = {
|
|||||||
'compress-images': 'Repository',
|
'compress-images': 'Repository',
|
||||||
'codeql-analysis': 'Repository',
|
'codeql-analysis': 'Repository',
|
||||||
|
|
||||||
|
// Security
|
||||||
|
'security-scan': 'Security',
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
'validate-inputs': 'Validation',
|
'validate-inputs': 'Validation',
|
||||||
};
|
};
|
||||||
@@ -120,6 +123,7 @@ const CATEGORY_ICONS = {
|
|||||||
Build: '🏗️',
|
Build: '🏗️',
|
||||||
Publishing: '🚀',
|
Publishing: '🚀',
|
||||||
Repository: '📦',
|
Repository: '📦',
|
||||||
|
Security: '🛡️',
|
||||||
Validation: '✅',
|
Validation: '✅',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -232,7 +236,7 @@ function generateCategoryTables(actions) {
|
|||||||
let output = '';
|
let output = '';
|
||||||
|
|
||||||
// Sort categories by priority
|
// Sort categories by priority
|
||||||
const categoryOrder = ['Setup', 'Utilities', 'Linting', 'Testing', 'Build', 'Publishing', 'Repository', 'Validation'];
|
const categoryOrder = ['Setup', 'Utilities', 'Linting', 'Testing', 'Build', 'Publishing', 'Repository', 'Security', 'Validation'];
|
||||||
|
|
||||||
for (const category of categoryOrder) {
|
for (const category of categoryOrder) {
|
||||||
if (!categories[category]) continue;
|
if (!categories[category]) continue;
|
||||||
|
|||||||
@@ -36,15 +36,17 @@ conventions:
|
|||||||
disable-linters: linter_list
|
disable-linters: linter_list
|
||||||
enable-linters: linter_list
|
enable-linters: linter_list
|
||||||
fail-on-error: boolean
|
fail-on-error: boolean
|
||||||
go-version: semantic_version
|
go-version: go_version
|
||||||
golangci-lint-version: semantic_version
|
golangci-lint-version: semantic_version
|
||||||
max-retries: numeric_range_1_10
|
max-retries: numeric_range_1_10
|
||||||
only-new-issues: branch_name
|
only-new-issues: boolean
|
||||||
report-format: report_format
|
report-format: report_format
|
||||||
timeout: numeric_range_1_3600
|
timeout: timeout_with_unit
|
||||||
token: github_token
|
token: github_token
|
||||||
working-directory: file_path
|
working-directory: file_path
|
||||||
overrides:
|
overrides:
|
||||||
|
disable-linters: linter_list
|
||||||
|
enable-linters: linter_list
|
||||||
go-version: go_version
|
go-version: go_version
|
||||||
only-new-issues: boolean
|
only-new-issues: boolean
|
||||||
timeout: timeout_with_unit
|
timeout: timeout_with_unit
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Validation rules for language-version-detect action
|
# Validation rules for language-version-detect action
|
||||||
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
||||||
# Schema version: 1.0
|
# Schema version: 1.0
|
||||||
# Coverage: 67% (2/3 inputs)
|
# Coverage: 100% (3/3 inputs)
|
||||||
#
|
#
|
||||||
# This file defines validation rules for the language-version-detect GitHub Action.
|
# This file defines validation rules for the language-version-detect GitHub Action.
|
||||||
# Rules are automatically applied by validate-inputs action when this
|
# Rules are automatically applied by validate-inputs action when this
|
||||||
@@ -21,16 +21,17 @@ optional_inputs:
|
|||||||
- token
|
- token
|
||||||
conventions:
|
conventions:
|
||||||
default-version: semantic_version
|
default-version: semantic_version
|
||||||
|
language: language_enum
|
||||||
token: github_token
|
token: github_token
|
||||||
overrides: {}
|
overrides: {}
|
||||||
statistics:
|
statistics:
|
||||||
total_inputs: 3
|
total_inputs: 3
|
||||||
validated_inputs: 2
|
validated_inputs: 3
|
||||||
skipped_inputs: 0
|
skipped_inputs: 0
|
||||||
coverage_percentage: 67
|
coverage_percentage: 100
|
||||||
validation_coverage: 67
|
validation_coverage: 100
|
||||||
auto_detected: true
|
auto_detected: true
|
||||||
manual_review_required: true
|
manual_review_required: false
|
||||||
quality_indicators:
|
quality_indicators:
|
||||||
has_required_inputs: true
|
has_required_inputs: true
|
||||||
has_token_validation: true
|
has_token_validation: true
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ optional_inputs:
|
|||||||
- token
|
- token
|
||||||
conventions:
|
conventions:
|
||||||
npm_token: github_token
|
npm_token: github_token
|
||||||
package-version: semantic_version
|
package-version: strict_semantic_version
|
||||||
registry-url: url
|
registry-url: url
|
||||||
scope: scope
|
scope: scope
|
||||||
token: github_token
|
token: github_token
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Validation rules for php-tests action
|
# Validation rules for php-tests action
|
||||||
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
||||||
# Schema version: 1.0
|
# Schema version: 1.0
|
||||||
# Coverage: 78% (7/9 inputs)
|
# Coverage: 89% (8/9 inputs)
|
||||||
#
|
#
|
||||||
# This file defines validation rules for the php-tests GitHub Action.
|
# This file defines validation rules for the php-tests GitHub Action.
|
||||||
# Rules are automatically applied by validate-inputs action when this
|
# Rules are automatically applied by validate-inputs action when this
|
||||||
@@ -27,7 +27,8 @@ optional_inputs:
|
|||||||
conventions:
|
conventions:
|
||||||
coverage: coverage_driver
|
coverage: coverage_driver
|
||||||
email: email
|
email: email
|
||||||
framework: boolean
|
extensions: php_extensions
|
||||||
|
framework: framework_mode
|
||||||
max-retries: numeric_range_1_10
|
max-retries: numeric_range_1_10
|
||||||
php-version: semantic_version
|
php-version: semantic_version
|
||||||
token: github_token
|
token: github_token
|
||||||
@@ -35,12 +36,12 @@ conventions:
|
|||||||
overrides: {}
|
overrides: {}
|
||||||
statistics:
|
statistics:
|
||||||
total_inputs: 9
|
total_inputs: 9
|
||||||
validated_inputs: 7
|
validated_inputs: 8
|
||||||
skipped_inputs: 0
|
skipped_inputs: 0
|
||||||
coverage_percentage: 78
|
coverage_percentage: 89
|
||||||
validation_coverage: 78
|
validation_coverage: 89
|
||||||
auto_detected: true
|
auto_detected: true
|
||||||
manual_review_required: true
|
manual_review_required: false
|
||||||
quality_indicators:
|
quality_indicators:
|
||||||
has_required_inputs: false
|
has_required_inputs: false
|
||||||
has_token_validation: true
|
has_token_validation: true
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Validation rules for prettier-lint action
|
# Validation rules for prettier-lint action
|
||||||
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
||||||
# Schema version: 1.0
|
# Schema version: 1.0
|
||||||
# Coverage: 86% (12/14 inputs)
|
# Coverage: 100% (14/14 inputs)
|
||||||
#
|
#
|
||||||
# This file defines validation rules for the prettier-lint GitHub Action.
|
# This file defines validation rules for the prettier-lint GitHub Action.
|
||||||
# Rules are automatically applied by validate-inputs action when this
|
# Rules are automatically applied by validate-inputs action when this
|
||||||
@@ -34,21 +34,24 @@ conventions:
|
|||||||
config-file: file_path
|
config-file: file_path
|
||||||
email: email
|
email: email
|
||||||
fail-on-error: boolean
|
fail-on-error: boolean
|
||||||
|
file-pattern: path_list
|
||||||
ignore-file: file_path
|
ignore-file: file_path
|
||||||
max-retries: numeric_range_1_10
|
max-retries: numeric_range_1_10
|
||||||
mode: mode_enum
|
mode: mode_enum
|
||||||
|
plugins: linter_list
|
||||||
prettier-version: semantic_version
|
prettier-version: semantic_version
|
||||||
report-format: report_format
|
report-format: report_format
|
||||||
token: github_token
|
token: github_token
|
||||||
username: username
|
username: username
|
||||||
working-directory: file_path
|
working-directory: file_path
|
||||||
overrides: {}
|
overrides:
|
||||||
|
mode: mode_enum
|
||||||
statistics:
|
statistics:
|
||||||
total_inputs: 14
|
total_inputs: 14
|
||||||
validated_inputs: 12
|
validated_inputs: 14
|
||||||
skipped_inputs: 0
|
skipped_inputs: 0
|
||||||
coverage_percentage: 86
|
coverage_percentage: 100
|
||||||
validation_coverage: 86
|
validation_coverage: 100
|
||||||
auto_detected: true
|
auto_detected: true
|
||||||
manual_review_required: false
|
manual_review_required: false
|
||||||
quality_indicators:
|
quality_indicators:
|
||||||
|
|||||||
82
security-scan/README.md
Normal file
82
security-scan/README.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# ivuorinen/actions/security-scan
|
||||||
|
|
||||||
|
## Security Scan
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
Comprehensive security scanning for GitHub Actions including actionlint,
|
||||||
|
Gitleaks (optional), and Trivy vulnerability scanning. Requires
|
||||||
|
'security-events: write' and 'contents: read' permissions in the workflow.
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
|
||||||
|
| name | description | required | default |
|
||||||
|
|----------------------|--------------------------------------------------------------|----------|----------------------|
|
||||||
|
| `gitleaks-license` | <p>Gitleaks license key (required for Gitleaks scanning)</p> | `false` | `""` |
|
||||||
|
| `gitleaks-config` | <p>Path to Gitleaks config file</p> | `false` | `.gitleaks.toml` |
|
||||||
|
| `trivy-severity` | <p>Severity levels to scan for (comma-separated)</p> | `false` | `CRITICAL,HIGH` |
|
||||||
|
| `trivy-scanners` | <p>Types of scanners to run (comma-separated)</p> | `false` | `vuln,config,secret` |
|
||||||
|
| `trivy-timeout` | <p>Timeout for Trivy scan</p> | `false` | `10m` |
|
||||||
|
| `actionlint-enabled` | <p>Enable actionlint scanning</p> | `false` | `true` |
|
||||||
|
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
|
||||||
|
|
||||||
|
### Outputs
|
||||||
|
|
||||||
|
| name | description |
|
||||||
|
|------------------------|-----------------------------------------------------|
|
||||||
|
| `has_trivy_results` | <p>Whether Trivy scan produced valid results</p> |
|
||||||
|
| `has_gitleaks_results` | <p>Whether Gitleaks scan produced valid results</p> |
|
||||||
|
| `total_issues` | <p>Total number of security issues found</p> |
|
||||||
|
| `critical_issues` | <p>Number of critical security issues found</p> |
|
||||||
|
|
||||||
|
### Runs
|
||||||
|
|
||||||
|
This action is a `composite` action.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: ivuorinen/actions/security-scan@main
|
||||||
|
with:
|
||||||
|
gitleaks-license:
|
||||||
|
# Gitleaks license key (required for Gitleaks scanning)
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: ""
|
||||||
|
|
||||||
|
gitleaks-config:
|
||||||
|
# Path to Gitleaks config file
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: .gitleaks.toml
|
||||||
|
|
||||||
|
trivy-severity:
|
||||||
|
# Severity levels to scan for (comma-separated)
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: CRITICAL,HIGH
|
||||||
|
|
||||||
|
trivy-scanners:
|
||||||
|
# Types of scanners to run (comma-separated)
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: vuln,config,secret
|
||||||
|
|
||||||
|
trivy-timeout:
|
||||||
|
# Timeout for Trivy scan
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: 10m
|
||||||
|
|
||||||
|
actionlint-enabled:
|
||||||
|
# Enable actionlint scanning
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: true
|
||||||
|
|
||||||
|
token:
|
||||||
|
# GitHub token for authentication
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: ""
|
||||||
|
```
|
||||||
282
security-scan/action.yml
Normal file
282
security-scan/action.yml
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
|
||||||
|
#
|
||||||
|
# REQUIRED PERMISSIONS (set these in your workflow file):
|
||||||
|
# permissions:
|
||||||
|
# security-events: write # Required for SARIF uploads
|
||||||
|
# contents: read # Required for repository access
|
||||||
|
#
|
||||||
|
---
|
||||||
|
name: Security Scan
|
||||||
|
description: |
|
||||||
|
Comprehensive security scanning for GitHub Actions including actionlint,
|
||||||
|
Gitleaks (optional), and Trivy vulnerability scanning. Requires
|
||||||
|
'security-events: write' and 'contents: read' permissions in the workflow.
|
||||||
|
author: Ismo Vuorinen
|
||||||
|
branding:
|
||||||
|
icon: shield
|
||||||
|
color: red
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
gitleaks-license:
|
||||||
|
description: 'Gitleaks license key (required for Gitleaks scanning)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
gitleaks-config:
|
||||||
|
description: 'Path to Gitleaks config file'
|
||||||
|
required: false
|
||||||
|
default: '.gitleaks.toml'
|
||||||
|
trivy-severity:
|
||||||
|
description: 'Severity levels to scan for (comma-separated)'
|
||||||
|
required: false
|
||||||
|
default: 'CRITICAL,HIGH'
|
||||||
|
trivy-scanners:
|
||||||
|
description: 'Types of scanners to run (comma-separated)'
|
||||||
|
required: false
|
||||||
|
default: 'vuln,config,secret'
|
||||||
|
trivy-timeout:
|
||||||
|
description: 'Timeout for Trivy scan'
|
||||||
|
required: false
|
||||||
|
default: '10m'
|
||||||
|
actionlint-enabled:
|
||||||
|
description: 'Enable actionlint scanning'
|
||||||
|
required: false
|
||||||
|
default: 'true'
|
||||||
|
token:
|
||||||
|
description: 'GitHub token for authentication'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
has_trivy_results:
|
||||||
|
description: 'Whether Trivy scan produced valid results'
|
||||||
|
value: ${{ steps.verify-sarif.outputs.has_trivy }}
|
||||||
|
has_gitleaks_results:
|
||||||
|
description: 'Whether Gitleaks scan produced valid results'
|
||||||
|
value: ${{ steps.verify-sarif.outputs.has_gitleaks }}
|
||||||
|
total_issues:
|
||||||
|
description: 'Total number of security issues found'
|
||||||
|
value: ${{ steps.analyze.outputs.total_issues }}
|
||||||
|
critical_issues:
|
||||||
|
description: 'Number of critical security issues found'
|
||||||
|
value: ${{ steps.analyze.outputs.critical_issues }}
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Validate Inputs
|
||||||
|
id: validate
|
||||||
|
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
|
||||||
|
with:
|
||||||
|
action-type: security-scan
|
||||||
|
gitleaks-license: ${{ inputs.gitleaks-license }}
|
||||||
|
gitleaks-config: ${{ inputs.gitleaks-config }}
|
||||||
|
trivy-severity: ${{ inputs.trivy-severity }}
|
||||||
|
trivy-scanners: ${{ inputs.trivy-scanners }}
|
||||||
|
trivy-timeout: ${{ inputs.trivy-timeout }}
|
||||||
|
actionlint-enabled: ${{ inputs.actionlint-enabled }}
|
||||||
|
token: ${{ inputs.token }}
|
||||||
|
|
||||||
|
- name: Check Required Configurations
|
||||||
|
id: check-configs
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Initialize all flags as false
|
||||||
|
{
|
||||||
|
printf '%s\n' "run_gitleaks=false"
|
||||||
|
printf '%s\n' "run_trivy=true"
|
||||||
|
printf '%s\n' "run_actionlint=${{ inputs.actionlint-enabled }}"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Check Gitleaks configuration and license
|
||||||
|
if [ -f "${{ inputs.gitleaks-config }}" ] && [ -n "${{ inputs.gitleaks-license }}" ]; then
|
||||||
|
printf 'Gitleaks config and license found\n'
|
||||||
|
printf '%s\n' "run_gitleaks=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
printf '::warning::Gitleaks config or license missing - skipping Gitleaks scan\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run actionlint
|
||||||
|
if: steps.check-configs.outputs.run_actionlint == 'true'
|
||||||
|
uses: raven-actions/actionlint@3a24062651993d40fed1019b58ac6fbdfbf276cc # v2.0.1
|
||||||
|
with:
|
||||||
|
cache: true
|
||||||
|
fail-on-error: true
|
||||||
|
shellcheck: false
|
||||||
|
|
||||||
|
- name: Run Gitleaks
|
||||||
|
if: steps.check-configs.outputs.run_gitleaks == 'true'
|
||||||
|
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ inputs.token || github.token }}
|
||||||
|
GITLEAKS_LICENSE: ${{ inputs.gitleaks-license }}
|
||||||
|
with:
|
||||||
|
config-path: ${{ inputs.gitleaks-config }}
|
||||||
|
report-format: sarif
|
||||||
|
report-path: gitleaks-report.sarif
|
||||||
|
|
||||||
|
- name: Run Trivy vulnerability scanner
|
||||||
|
if: steps.check-configs.outputs.run_trivy == 'true'
|
||||||
|
uses: aquasecurity/trivy-action@a11da62073708815958ea6d84f5650c78a3ef85b # master
|
||||||
|
with:
|
||||||
|
scan-type: 'fs'
|
||||||
|
scanners: ${{ inputs.trivy-scanners }}
|
||||||
|
format: 'sarif'
|
||||||
|
output: 'trivy-results.sarif'
|
||||||
|
severity: ${{ inputs.trivy-severity }}
|
||||||
|
timeout: ${{ inputs.trivy-timeout }}
|
||||||
|
|
||||||
|
- name: Verify SARIF files
|
||||||
|
id: verify-sarif
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Initialize outputs
|
||||||
|
{
|
||||||
|
printf '%s\n' "has_trivy=false"
|
||||||
|
printf '%s\n' "has_gitleaks=false"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Check Trivy results
|
||||||
|
if [ -f "trivy-results.sarif" ]; then
|
||||||
|
if jq -e . <"trivy-results.sarif" >/dev/null 2>&1; then
|
||||||
|
printf '%s\n' "has_trivy=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
printf '::warning::Trivy SARIF file exists but is not valid JSON\n'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Gitleaks results if it ran
|
||||||
|
if [ "${{ steps.check-configs.outputs.run_gitleaks }}" = "true" ]; then
|
||||||
|
if [ -f "gitleaks-report.sarif" ]; then
|
||||||
|
if jq -e . <"gitleaks-report.sarif" >/dev/null 2>&1; then
|
||||||
|
printf '%s\n' "has_gitleaks=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
printf '::warning::Gitleaks SARIF file exists but is not valid JSON\n'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload Trivy results
|
||||||
|
if: steps.verify-sarif.outputs.has_trivy == 'true'
|
||||||
|
uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||||
|
with:
|
||||||
|
sarif_file: 'trivy-results.sarif'
|
||||||
|
category: 'trivy'
|
||||||
|
|
||||||
|
- name: Upload Gitleaks results
|
||||||
|
if: steps.verify-sarif.outputs.has_gitleaks == 'true'
|
||||||
|
uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||||
|
with:
|
||||||
|
sarif_file: 'gitleaks-report.sarif'
|
||||||
|
category: 'gitleaks'
|
||||||
|
|
||||||
|
- name: Archive security reports
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
|
with:
|
||||||
|
name: security-reports-${{ github.run_id }}
|
||||||
|
path: |
|
||||||
|
${{ steps.verify-sarif.outputs.has_trivy == 'true' && 'trivy-results.sarif' || '' }}
|
||||||
|
${{ steps.verify-sarif.outputs.has_gitleaks == 'true' && 'gitleaks-report.sarif' || '' }}
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Analyze Results
|
||||||
|
id: analyze
|
||||||
|
if: always()
|
||||||
|
shell: node {0}
|
||||||
|
run: |
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
try {
|
||||||
|
let totalIssues = 0;
|
||||||
|
let criticalIssues = 0;
|
||||||
|
|
||||||
|
const analyzeSarif = (file, tool) => {
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
console.log(`No results file found for ${tool}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sarif = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||||
|
return sarif.runs.reduce((acc, run) => {
|
||||||
|
if (!run.results) return acc;
|
||||||
|
|
||||||
|
const critical = run.results.filter(r =>
|
||||||
|
r.level === 'error' ||
|
||||||
|
r.level === 'critical' ||
|
||||||
|
(r.ruleId || '').toLowerCase().includes('critical')
|
||||||
|
).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: acc.total + run.results.length,
|
||||||
|
critical: acc.critical + critical
|
||||||
|
};
|
||||||
|
}, { total: 0, critical: 0 });
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error analyzing ${tool} results: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only analyze results from tools that ran successfully
|
||||||
|
const results = {
|
||||||
|
trivy: '${{ steps.verify-sarif.outputs.has_trivy }}' === 'true' ?
|
||||||
|
analyzeSarif('trivy-results.sarif', 'trivy') : null,
|
||||||
|
gitleaks: '${{ steps.verify-sarif.outputs.has_gitleaks }}' === 'true' ?
|
||||||
|
analyzeSarif('gitleaks-report.sarif', 'gitleaks') : null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aggregate results
|
||||||
|
Object.entries(results).forEach(([tool, result]) => {
|
||||||
|
if (result) {
|
||||||
|
totalIssues += result.total;
|
||||||
|
criticalIssues += result.critical;
|
||||||
|
console.log(`${tool}: ${result.total} total, ${result.critical} critical issues`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create summary
|
||||||
|
const summary = `## Security Scan Summary
|
||||||
|
|
||||||
|
- Total Issues Found: ${totalIssues}
|
||||||
|
- Critical Issues: ${criticalIssues}
|
||||||
|
|
||||||
|
### Tool Breakdown
|
||||||
|
${Object.entries(results)
|
||||||
|
.filter(([_, r]) => r)
|
||||||
|
.map(([tool, r]) =>
|
||||||
|
`- ${tool}: ${r.total} total, ${r.critical} critical`
|
||||||
|
).join('\n')}
|
||||||
|
|
||||||
|
### Tools Run Status
|
||||||
|
- Actionlint: ${{ steps.check-configs.outputs.run_actionlint }}
|
||||||
|
- Trivy: ${{ steps.verify-sarif.outputs.has_trivy }}
|
||||||
|
- Gitleaks: ${{ steps.check-configs.outputs.run_gitleaks }}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Set outputs using GITHUB_OUTPUT
|
||||||
|
const outputFile = process.env.GITHUB_OUTPUT;
|
||||||
|
if (outputFile) {
|
||||||
|
fs.appendFileSync(outputFile, `total_issues=${totalIssues}\n`);
|
||||||
|
fs.appendFileSync(outputFile, `critical_issues=${criticalIssues}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add job summary using GITHUB_STEP_SUMMARY
|
||||||
|
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
|
||||||
|
if (summaryFile) {
|
||||||
|
fs.appendFileSync(summaryFile, summary + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail if critical issues found
|
||||||
|
if (criticalIssues > 0) {
|
||||||
|
console.error(`Found ${criticalIssues} critical security issues`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Analysis failed: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
55
security-scan/rules.yml
Normal file
55
security-scan/rules.yml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
# Validation rules for security-scan action
|
||||||
|
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
||||||
|
# Schema version: 1.0
|
||||||
|
# Coverage: 86% (6/7 inputs)
|
||||||
|
#
|
||||||
|
# This file defines validation rules for the security-scan GitHub Action.
|
||||||
|
# Rules are automatically applied by validate-inputs action when this
|
||||||
|
# action is used.
|
||||||
|
#
|
||||||
|
|
||||||
|
schema_version: '1.0'
|
||||||
|
action: security-scan
|
||||||
|
description: |
|
||||||
|
Comprehensive security scanning for GitHub Actions including actionlint,
|
||||||
|
Gitleaks (optional), and Trivy vulnerability scanning. Requires
|
||||||
|
'security-events: write' and 'contents: read' permissions in the workflow.
|
||||||
|
generator_version: 1.0.0
|
||||||
|
required_inputs: []
|
||||||
|
optional_inputs:
|
||||||
|
- actionlint-enabled
|
||||||
|
- gitleaks-config
|
||||||
|
- gitleaks-license
|
||||||
|
- token
|
||||||
|
- trivy-scanners
|
||||||
|
- trivy-severity
|
||||||
|
- trivy-timeout
|
||||||
|
conventions:
|
||||||
|
actionlint-enabled: boolean
|
||||||
|
gitleaks-config: file_path
|
||||||
|
token: github_token
|
||||||
|
trivy-scanners: scanner_list
|
||||||
|
trivy-severity: severity_enum
|
||||||
|
trivy-timeout: timeout_with_unit
|
||||||
|
overrides:
|
||||||
|
actionlint-enabled: boolean
|
||||||
|
gitleaks-config: file_path
|
||||||
|
token: github_token
|
||||||
|
trivy-scanners: scanner_list
|
||||||
|
trivy-severity: severity_enum
|
||||||
|
trivy-timeout: timeout_with_unit
|
||||||
|
statistics:
|
||||||
|
total_inputs: 7
|
||||||
|
validated_inputs: 6
|
||||||
|
skipped_inputs: 0
|
||||||
|
coverage_percentage: 86
|
||||||
|
validation_coverage: 86
|
||||||
|
auto_detected: true
|
||||||
|
manual_review_required: false
|
||||||
|
quality_indicators:
|
||||||
|
has_required_inputs: false
|
||||||
|
has_token_validation: true
|
||||||
|
has_version_validation: false
|
||||||
|
has_file_validation: true
|
||||||
|
has_security_validation: true
|
||||||
@@ -8,56 +8,62 @@ Centralized Python-based input validation for GitHub Actions with PCRE regex sup
|
|||||||
|
|
||||||
### Inputs
|
### Inputs
|
||||||
|
|
||||||
| name | description | required | default |
|
| name | description | required | default |
|
||||||
|---------------------|-------------------------------------------------------------------------------------|----------|---------|
|
|----------------------|-------------------------------------------------------------------------------------|----------|---------|
|
||||||
| `action` | <p>Action name to validate (alias for action-type)</p> | `false` | `""` |
|
| `action` | <p>Action name to validate (alias for action-type)</p> | `false` | `""` |
|
||||||
| `action-type` | <p>Type of action to validate (e.g., csharp-publish, docker-build, eslint-lint)</p> | `false` | `""` |
|
| `action-type` | <p>Type of action to validate (e.g., csharp-publish, docker-build, eslint-lint)</p> | `false` | `""` |
|
||||||
| `rules-file` | <p>Path to validation rules file</p> | `false` | `""` |
|
| `rules-file` | <p>Path to validation rules file</p> | `false` | `""` |
|
||||||
| `fail-on-error` | <p>Whether to fail on validation errors</p> | `false` | `true` |
|
| `fail-on-error` | <p>Whether to fail on validation errors</p> | `false` | `true` |
|
||||||
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
|
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
|
||||||
| `namespace` | <p>Namespace/username for validation</p> | `false` | `""` |
|
| `namespace` | <p>Namespace/username for validation</p> | `false` | `""` |
|
||||||
| `email` | <p>Email address for validation</p> | `false` | `""` |
|
| `email` | <p>Email address for validation</p> | `false` | `""` |
|
||||||
| `username` | <p>Username for validation</p> | `false` | `""` |
|
| `username` | <p>Username for validation</p> | `false` | `""` |
|
||||||
| `dotnet-version` | <p>.NET version string</p> | `false` | `""` |
|
| `dotnet-version` | <p>.NET version string</p> | `false` | `""` |
|
||||||
| `terraform-version` | <p>Terraform version string</p> | `false` | `""` |
|
| `terraform-version` | <p>Terraform version string</p> | `false` | `""` |
|
||||||
| `tflint-version` | <p>TFLint version string</p> | `false` | `""` |
|
| `tflint-version` | <p>TFLint version string</p> | `false` | `""` |
|
||||||
| `node-version` | <p>Node.js version string</p> | `false` | `""` |
|
| `node-version` | <p>Node.js version string</p> | `false` | `""` |
|
||||||
| `force-version` | <p>Force version override</p> | `false` | `""` |
|
| `force-version` | <p>Force version override</p> | `false` | `""` |
|
||||||
| `default-version` | <p>Default version fallback</p> | `false` | `""` |
|
| `default-version` | <p>Default version fallback</p> | `false` | `""` |
|
||||||
| `image-name` | <p>Docker image name</p> | `false` | `""` |
|
| `image-name` | <p>Docker image name</p> | `false` | `""` |
|
||||||
| `tag` | <p>Docker image tag</p> | `false` | `""` |
|
| `tag` | <p>Docker image tag</p> | `false` | `""` |
|
||||||
| `architectures` | <p>Target architectures</p> | `false` | `""` |
|
| `architectures` | <p>Target architectures</p> | `false` | `""` |
|
||||||
| `dockerfile` | <p>Dockerfile path</p> | `false` | `""` |
|
| `dockerfile` | <p>Dockerfile path</p> | `false` | `""` |
|
||||||
| `context` | <p>Docker build context</p> | `false` | `""` |
|
| `context` | <p>Docker build context</p> | `false` | `""` |
|
||||||
| `build-args` | <p>Docker build arguments</p> | `false` | `""` |
|
| `build-args` | <p>Docker build arguments</p> | `false` | `""` |
|
||||||
| `buildx-version` | <p>Docker Buildx version</p> | `false` | `""` |
|
| `buildx-version` | <p>Docker Buildx version</p> | `false` | `""` |
|
||||||
| `max-retries` | <p>Maximum retry attempts</p> | `false` | `""` |
|
| `max-retries` | <p>Maximum retry attempts</p> | `false` | `""` |
|
||||||
| `image-quality` | <p>Image quality percentage</p> | `false` | `""` |
|
| `image-quality` | <p>Image quality percentage</p> | `false` | `""` |
|
||||||
| `png-quality` | <p>PNG quality percentage</p> | `false` | `""` |
|
| `png-quality` | <p>PNG quality percentage</p> | `false` | `""` |
|
||||||
| `parallel-builds` | <p>Number of parallel builds</p> | `false` | `""` |
|
| `parallel-builds` | <p>Number of parallel builds</p> | `false` | `""` |
|
||||||
| `days-before-stale` | <p>Number of days before marking as stale</p> | `false` | `""` |
|
| `days-before-stale` | <p>Number of days before marking as stale</p> | `false` | `""` |
|
||||||
| `days-before-close` | <p>Number of days before closing stale items</p> | `false` | `""` |
|
| `days-before-close` | <p>Number of days before closing stale items</p> | `false` | `""` |
|
||||||
| `pre-commit-config` | <p>Pre-commit configuration file path</p> | `false` | `""` |
|
| `pre-commit-config` | <p>Pre-commit configuration file path</p> | `false` | `""` |
|
||||||
| `base-branch` | <p>Base branch name</p> | `false` | `""` |
|
| `base-branch` | <p>Base branch name</p> | `false` | `""` |
|
||||||
| `dry-run` | <p>Dry run mode</p> | `false` | `""` |
|
| `dry-run` | <p>Dry run mode</p> | `false` | `""` |
|
||||||
| `is_fiximus` | <p>Use Fiximus bot</p> | `false` | `""` |
|
| `is_fiximus` | <p>Use Fiximus bot</p> | `false` | `""` |
|
||||||
| `prefix` | <p>Release tag prefix</p> | `false` | `""` |
|
| `prefix` | <p>Release tag prefix</p> | `false` | `""` |
|
||||||
| `language` | <p>Language to analyze (for CodeQL)</p> | `false` | `""` |
|
| `language` | <p>Language to analyze (for CodeQL)</p> | `false` | `""` |
|
||||||
| `queries` | <p>CodeQL queries to run</p> | `false` | `""` |
|
| `queries` | <p>CodeQL queries to run</p> | `false` | `""` |
|
||||||
| `packs` | <p>CodeQL query packs</p> | `false` | `""` |
|
| `packs` | <p>CodeQL query packs</p> | `false` | `""` |
|
||||||
| `config-file` | <p>CodeQL configuration file path</p> | `false` | `""` |
|
| `config-file` | <p>CodeQL configuration file path</p> | `false` | `""` |
|
||||||
| `config` | <p>CodeQL configuration YAML string</p> | `false` | `""` |
|
| `config` | <p>CodeQL configuration YAML string</p> | `false` | `""` |
|
||||||
| `build-mode` | <p>Build mode for compiled languages</p> | `false` | `""` |
|
| `build-mode` | <p>Build mode for compiled languages</p> | `false` | `""` |
|
||||||
| `source-root` | <p>Source code root directory</p> | `false` | `""` |
|
| `source-root` | <p>Source code root directory</p> | `false` | `""` |
|
||||||
| `category` | <p>Analysis category</p> | `false` | `""` |
|
| `category` | <p>Analysis category</p> | `false` | `""` |
|
||||||
| `checkout-ref` | <p>Git reference to checkout</p> | `false` | `""` |
|
| `checkout-ref` | <p>Git reference to checkout</p> | `false` | `""` |
|
||||||
| `working-directory` | <p>Working directory for analysis</p> | `false` | `""` |
|
| `working-directory` | <p>Working directory for analysis</p> | `false` | `""` |
|
||||||
| `upload-results` | <p>Upload results to GitHub Security</p> | `false` | `""` |
|
| `upload-results` | <p>Upload results to GitHub Security</p> | `false` | `""` |
|
||||||
| `ram` | <p>Memory in MB for CodeQL</p> | `false` | `""` |
|
| `ram` | <p>Memory in MB for CodeQL</p> | `false` | `""` |
|
||||||
| `threads` | <p>Number of threads for CodeQL</p> | `false` | `""` |
|
| `threads` | <p>Number of threads for CodeQL</p> | `false` | `""` |
|
||||||
| `output` | <p>Output path for SARIF results</p> | `false` | `""` |
|
| `output` | <p>Output path for SARIF results</p> | `false` | `""` |
|
||||||
| `skip-queries` | <p>Skip running queries</p> | `false` | `""` |
|
| `skip-queries` | <p>Skip running queries</p> | `false` | `""` |
|
||||||
| `add-snippets` | <p>Add code snippets to SARIF</p> | `false` | `""` |
|
| `add-snippets` | <p>Add code snippets to SARIF</p> | `false` | `""` |
|
||||||
|
| `gitleaks-license` | <p>Gitleaks license key</p> | `false` | `""` |
|
||||||
|
| `gitleaks-config` | <p>Gitleaks configuration file path</p> | `false` | `""` |
|
||||||
|
| `trivy-severity` | <p>Trivy severity levels to scan</p> | `false` | `""` |
|
||||||
|
| `trivy-scanners` | <p>Trivy scanner types to run</p> | `false` | `""` |
|
||||||
|
| `trivy-timeout` | <p>Trivy scan timeout</p> | `false` | `""` |
|
||||||
|
| `actionlint-enabled` | <p>Enable actionlint scanning</p> | `false` | `""` |
|
||||||
|
|
||||||
### Outputs
|
### Outputs
|
||||||
|
|
||||||
@@ -365,4 +371,40 @@ This action is a `composite` action.
|
|||||||
#
|
#
|
||||||
# Required: false
|
# Required: false
|
||||||
# Default: ""
|
# Default: ""
|
||||||
|
|
||||||
|
gitleaks-license:
|
||||||
|
# Gitleaks license key
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: ""
|
||||||
|
|
||||||
|
gitleaks-config:
|
||||||
|
# Gitleaks configuration file path
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: ""
|
||||||
|
|
||||||
|
trivy-severity:
|
||||||
|
# Trivy severity levels to scan
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: ""
|
||||||
|
|
||||||
|
trivy-scanners:
|
||||||
|
# Trivy scanner types to run
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: ""
|
||||||
|
|
||||||
|
trivy-timeout:
|
||||||
|
# Trivy scan timeout
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: ""
|
||||||
|
|
||||||
|
actionlint-enabled:
|
||||||
|
# Enable actionlint scanning
|
||||||
|
#
|
||||||
|
# Required: false
|
||||||
|
# Default: ""
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -173,6 +173,26 @@ inputs:
|
|||||||
description: 'Add code snippets to SARIF'
|
description: 'Add code snippets to SARIF'
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
|
# Security-scan specific inputs
|
||||||
|
gitleaks-license:
|
||||||
|
description: 'Gitleaks license key'
|
||||||
|
required: false
|
||||||
|
gitleaks-config:
|
||||||
|
description: 'Gitleaks configuration file path'
|
||||||
|
required: false
|
||||||
|
trivy-severity:
|
||||||
|
description: 'Trivy severity levels to scan'
|
||||||
|
required: false
|
||||||
|
trivy-scanners:
|
||||||
|
description: 'Trivy scanner types to run'
|
||||||
|
required: false
|
||||||
|
trivy-timeout:
|
||||||
|
description: 'Trivy scan timeout'
|
||||||
|
required: false
|
||||||
|
actionlint-enabled:
|
||||||
|
description: 'Enable actionlint scanning'
|
||||||
|
required: false
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
validation-status:
|
validation-status:
|
||||||
description: 'Overall validation status (success/failure)'
|
description: 'Overall validation status (success/failure)'
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class ValidationRuleGenerator:
|
|||||||
"prefix": re.compile(r"\b(prefix|tag[_-]?prefix)\b", re.IGNORECASE),
|
"prefix": re.compile(r"\b(prefix|tag[_-]?prefix)\b", re.IGNORECASE),
|
||||||
# Boolean patterns (broad, should be lower priority)
|
# Boolean patterns (broad, should be lower priority)
|
||||||
"boolean": re.compile(
|
"boolean": re.compile(
|
||||||
r"\b(dry-?run|verbose|enable|disable|auto|skip|force|cache|provenance|sbom|scan|sign|fail[_-]?on[_-]?error|nightly)\b",
|
r"\b(dry-?run|verbose|enable|disable|auto|skip|force|cache|provenance|sbom|scan|sign|push|fail[_-]?on[_-]?error|nightly)\b",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
),
|
),
|
||||||
# File extensions pattern
|
# File extensions pattern
|
||||||
@@ -160,36 +160,36 @@ class ValidationRuleGenerator:
|
|||||||
"npm_token": "github_token",
|
"npm_token": "github_token",
|
||||||
"password": "github_token",
|
"password": "github_token",
|
||||||
# Complex fields that should skip validation
|
# Complex fields that should skip validation
|
||||||
"build-args": None, # Can be empty
|
"build-args": "key_value_list", # Docker build arguments (KEY=VALUE format)
|
||||||
"context": None, # Default handled
|
"context": "file_path", # Build context path
|
||||||
"cache-from": None, # Complex cache syntax
|
"cache-from": "cache_config", # Docker cache configuration
|
||||||
"cache-export": None, # Complex cache syntax
|
"cache-export": "cache_config", # Docker cache configuration
|
||||||
"cache-import": None, # Complex cache syntax
|
"cache-import": "cache_config", # Docker cache configuration
|
||||||
"build-contexts": None, # Complex syntax
|
"build-contexts": "key_value_list", # Docker build contexts (KEY=VALUE format)
|
||||||
"secrets": None, # Complex syntax
|
"secrets": "key_value_list", # Docker secrets (KEY=VALUE format)
|
||||||
"platform-build-args": None, # JSON format
|
"platform-build-args": "json_format", # JSON format for platform-specific args
|
||||||
"extensions": None, # PHP extensions list
|
"extensions": "php_extensions", # PHP extensions list
|
||||||
"tools": None, # PHP tools list
|
"tools": "linter_list", # PHP tools list - same pattern as linters
|
||||||
|
"framework": "framework_mode", # PHP framework mode (auto, laravel, generic)
|
||||||
"args": None, # Composer args
|
"args": None, # Composer args
|
||||||
"stability": None, # Composer stability
|
"stability": None, # Composer stability
|
||||||
"registry-url": "url", # URL format
|
"registry-url": "url", # URL format
|
||||||
"scope": "scope", # NPM scope
|
"scope": "scope", # NPM scope
|
||||||
"plugins": None, # Prettier plugins
|
"plugins": "linter_list", # Prettier plugins - same pattern as linters
|
||||||
"file-extensions": "file_extensions", # File extension list
|
"file-extensions": "file_extensions", # File extension list
|
||||||
"file-pattern": None, # Glob pattern
|
"file-pattern": "path_list", # Glob pattern for file paths
|
||||||
"enable-linters": None, # Linter list
|
"enable-linters": "linter_list", # Linter list
|
||||||
"disable-linters": None, # Linter list
|
"disable-linters": "linter_list", # Linter list
|
||||||
"success-codes": None, # Exit code list
|
"success-codes": "exit_code_list", # Exit code list
|
||||||
"retry-codes": None, # Exit code list
|
"retry-codes": "exit_code_list", # Exit code list
|
||||||
"ignore-paths": None, # Path patterns
|
"ignore-paths": "path_list", # Path patterns to ignore
|
||||||
"key-files": None, # Cache key files
|
"key-files": "path_list", # Cache key files (paths)
|
||||||
"restore-keys": None, # Cache restore keys
|
"restore-keys": "path_list", # Cache restore keys (paths)
|
||||||
"env-vars": None, # Environment variables
|
"env-vars": "key_value_list", # Environment variables (KEY=VALUE format)
|
||||||
# Action-specific fields that need special handling
|
# Action-specific fields that need special handling
|
||||||
"type": None, # Cache type enum (npm, composer, go, etc.) - complex enum,
|
"type": None, # Cache type enum (npm, composer, go, etc.) - complex enum,
|
||||||
# skip validation
|
# skip validation
|
||||||
"paths": None, # File paths for caching (comma-separated) - complex format,
|
"paths": "path_list", # File paths for caching (comma-separated)
|
||||||
# skip validation
|
|
||||||
"command": None, # Shell command - complex format, skip validation for safety
|
"command": None, # Shell command - complex format, skip validation for safety
|
||||||
"backoff-strategy": None, # Retry strategy enum - complex enum, skip validation
|
"backoff-strategy": None, # Retry strategy enum - complex enum, skip validation
|
||||||
"shell": None, # Shell type enum - simple enum, skip validation
|
"shell": None, # Shell type enum - simple enum, skip validation
|
||||||
@@ -199,10 +199,13 @@ class ValidationRuleGenerator:
|
|||||||
"retry-delay": "numeric_range_1_300", # Retry delay should support higher values
|
"retry-delay": "numeric_range_1_300", # Retry delay should support higher values
|
||||||
"max-warnings": "numeric_range_0_10000",
|
"max-warnings": "numeric_range_0_10000",
|
||||||
# version-file-parser specific fields
|
# version-file-parser specific fields
|
||||||
"language": None, # Simple enum (node, php, python, go, dotnet)
|
|
||||||
"tool-versions-key": None, # Simple string (nodejs, python, php, golang, dotnet)
|
"tool-versions-key": None, # Simple string (nodejs, python, php, golang, dotnet)
|
||||||
"dockerfile-image": None, # Simple string (node, python, php, golang, dotnet)
|
"dockerfile-image": None, # Simple string (node, python, php, golang, dotnet)
|
||||||
"validation-regex": "regex_pattern", # Regex pattern - validate for ReDoS
|
"validation-regex": "regex_pattern", # Regex pattern - validate for ReDoS
|
||||||
|
# Docker network mode
|
||||||
|
"network": "network_mode", # Docker network mode (host, none, default)
|
||||||
|
# Language enum for version detection
|
||||||
|
"language": "language_enum", # Language type (php, python, go, dotnet)
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_action_directories(self) -> list[str]:
|
def get_action_directories(self) -> list[str]:
|
||||||
@@ -314,7 +317,6 @@ class ValidationRuleGenerator:
|
|||||||
"docker-publish": {
|
"docker-publish": {
|
||||||
"registry": "registry_enum",
|
"registry": "registry_enum",
|
||||||
"cache-mode": "cache_mode",
|
"cache-mode": "cache_mode",
|
||||||
"platforms": None, # Skip validation - complex platform format
|
|
||||||
},
|
},
|
||||||
"docker-publish-hub": {
|
"docker-publish-hub": {
|
||||||
"password": "docker_password",
|
"password": "docker_password",
|
||||||
@@ -354,26 +356,28 @@ class ValidationRuleGenerator:
|
|||||||
"prettier-lint": {
|
"prettier-lint": {
|
||||||
"mode": "mode_enum",
|
"mode": "mode_enum",
|
||||||
},
|
},
|
||||||
|
"security-scan": {
|
||||||
|
"gitleaks-config": "file_path",
|
||||||
|
"trivy-severity": "severity_enum",
|
||||||
|
"trivy-scanners": "scanner_list",
|
||||||
|
"trivy-timeout": "timeout_with_unit",
|
||||||
|
"actionlint-enabled": "boolean",
|
||||||
|
"token": "github_token",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if action_name in action_overrides:
|
if action_name in action_overrides:
|
||||||
# Apply overrides for existing conventions
|
# Apply overrides for existing conventions
|
||||||
overrides.update(
|
|
||||||
{
|
|
||||||
input_name: override_value
|
|
||||||
for input_name, override_value in action_overrides[action_name].items()
|
|
||||||
if input_name in conventions
|
|
||||||
},
|
|
||||||
)
|
|
||||||
# Add missing inputs from overrides to conventions
|
|
||||||
for input_name, override_value in action_overrides[action_name].items():
|
for input_name, override_value in action_overrides[action_name].items():
|
||||||
if input_name not in conventions and input_name in action_data["inputs"]:
|
if input_name in action_data["inputs"]:
|
||||||
|
overrides[input_name] = override_value
|
||||||
|
# Update conventions to match override (or set to None if skipped)
|
||||||
conventions[input_name] = override_value
|
conventions[input_name] = override_value
|
||||||
|
|
||||||
# Calculate statistics
|
# Calculate statistics
|
||||||
total_inputs = len(action_data["inputs"])
|
total_inputs = len(action_data["inputs"])
|
||||||
validated_inputs = len(conventions)
|
validated_inputs = sum(1 for v in conventions.values() if v is not None)
|
||||||
skipped_inputs = sum(1 for v in overrides.values() if v is None)
|
skipped_inputs = sum(1 for v in conventions.values() if v is None)
|
||||||
coverage = round((validated_inputs / total_inputs) * 100) if total_inputs > 0 else 0
|
coverage = round((validated_inputs / total_inputs) * 100) if total_inputs > 0 else 0
|
||||||
|
|
||||||
# Generate rules object with enhanced metadata
|
# Generate rules object with enhanced metadata
|
||||||
@@ -432,8 +436,20 @@ class ValidationRuleGenerator:
|
|||||||
|
|
||||||
# Use a custom yaml dumper to ensure proper indentation
|
# Use a custom yaml dumper to ensure proper indentation
|
||||||
class CustomYamlDumper(yaml.SafeDumper):
|
class CustomYamlDumper(yaml.SafeDumper):
|
||||||
def increase_indent(self, flow: bool = False, *, indentless: bool = False) -> None: # noqa: FBT001, FBT002
|
def increase_indent(self, flow: bool = False, *, indentless: bool = False) -> None: # noqa: FBT001, FBT002, ARG002 # type: ignore[override]
|
||||||
return super().increase_indent(flow, indentless=indentless)
|
return super().increase_indent(flow, False)
|
||||||
|
|
||||||
|
def choose_scalar_style(self):
|
||||||
|
"""Choose appropriate quote style based on string content."""
|
||||||
|
if hasattr(self, "event") and hasattr(self.event, "value") and self.event.value: # type: ignore[attr-defined]
|
||||||
|
value = self.event.value # type: ignore[attr-defined]
|
||||||
|
# Use literal block style for multiline strings
|
||||||
|
if "\n" in value:
|
||||||
|
return "|"
|
||||||
|
# Use double quotes for strings with single quotes
|
||||||
|
if "'" in value:
|
||||||
|
return '"'
|
||||||
|
return super().choose_scalar_style()
|
||||||
|
|
||||||
yaml_content = yaml.dump(
|
yaml_content = yaml.dump(
|
||||||
rules,
|
rules,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -274,6 +274,71 @@ class TestDockerValidator:
|
|||||||
result = self.validator.validate_inputs(inputs)
|
result = self.validator.validate_inputs(inputs)
|
||||||
assert isinstance(result, bool)
|
assert isinstance(result, bool)
|
||||||
|
|
||||||
|
def test_validate_registry_valid(self):
|
||||||
|
"""Test registry enum validation with valid values."""
|
||||||
|
valid_registries = [
|
||||||
|
"dockerhub",
|
||||||
|
"github",
|
||||||
|
"both",
|
||||||
|
]
|
||||||
|
|
||||||
|
for registry in valid_registries:
|
||||||
|
self.validator.errors = []
|
||||||
|
result = self.validator.validate_registry(registry)
|
||||||
|
assert result is True, f"Should accept registry: {registry}"
|
||||||
|
|
||||||
|
def test_validate_registry_invalid(self):
|
||||||
|
"""Test registry enum validation with invalid values."""
|
||||||
|
invalid_registries = [
|
||||||
|
"", # Empty
|
||||||
|
" ", # Whitespace only
|
||||||
|
"docker", # Wrong value (should be dockerhub)
|
||||||
|
"hub", # Wrong value
|
||||||
|
"ghcr", # Wrong value
|
||||||
|
"gcr", # Wrong value
|
||||||
|
"both,github", # Comma-separated not allowed
|
||||||
|
"DOCKERHUB", # Uppercase
|
||||||
|
"DockerHub", # Mixed case
|
||||||
|
"docker hub", # Space
|
||||||
|
"github.com", # Full URL not allowed
|
||||||
|
]
|
||||||
|
|
||||||
|
for registry in invalid_registries:
|
||||||
|
self.validator.errors = []
|
||||||
|
result = self.validator.validate_registry(registry)
|
||||||
|
assert result is False, f"Should reject registry: {registry}"
|
||||||
|
|
||||||
|
def test_validate_sbom_format_valid(self):
|
||||||
|
"""Test SBOM format validation with valid values."""
|
||||||
|
valid_formats = [
|
||||||
|
"spdx-json",
|
||||||
|
"cyclonedx-json",
|
||||||
|
"", # Empty is optional
|
||||||
|
]
|
||||||
|
|
||||||
|
for sbom_format in valid_formats:
|
||||||
|
self.validator.errors = []
|
||||||
|
result = self.validator.validate_sbom_format(sbom_format)
|
||||||
|
assert result is True, f"Should accept SBOM format: {sbom_format}"
|
||||||
|
|
||||||
|
def test_validate_sbom_format_invalid(self):
|
||||||
|
"""Test SBOM format validation with invalid values."""
|
||||||
|
invalid_formats = [
|
||||||
|
"spdx", # Missing -json suffix
|
||||||
|
"cyclonedx", # Missing -json suffix
|
||||||
|
"json", # Just json
|
||||||
|
"spdx-xml", # Wrong format
|
||||||
|
"cyclonedx-xml", # Wrong format
|
||||||
|
"SPDX-JSON", # Uppercase
|
||||||
|
"spdx json", # Space
|
||||||
|
"invalid", # Invalid value
|
||||||
|
]
|
||||||
|
|
||||||
|
for sbom_format in invalid_formats:
|
||||||
|
self.validator.errors = []
|
||||||
|
result = self.validator.validate_sbom_format(sbom_format)
|
||||||
|
assert result is False, f"Should reject SBOM format: {sbom_format}"
|
||||||
|
|
||||||
def test_empty_values_handling(self):
|
def test_empty_values_handling(self):
|
||||||
"""Test that empty values are handled appropriately."""
|
"""Test that empty values are handled appropriately."""
|
||||||
# Some Docker fields might be required, others optional
|
# Some Docker fields might be required, others optional
|
||||||
@@ -281,3 +346,5 @@ class TestDockerValidator:
|
|||||||
assert isinstance(self.validator.validate_docker_tag(""), bool)
|
assert isinstance(self.validator.validate_docker_tag(""), bool)
|
||||||
assert isinstance(self.validator.validate_architectures(""), bool)
|
assert isinstance(self.validator.validate_architectures(""), bool)
|
||||||
assert isinstance(self.validator.validate_prefix(""), bool)
|
assert isinstance(self.validator.validate_prefix(""), bool)
|
||||||
|
# Registry should reject empty values
|
||||||
|
assert self.validator.validate_registry("") is False
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class TestValidationRuleGenerator:
|
|||||||
generator = ValidationRuleGenerator()
|
generator = ValidationRuleGenerator()
|
||||||
|
|
||||||
# Test special cases from the mapping
|
# Test special cases from the mapping
|
||||||
assert generator.detect_validation_type("build-args", {}) is None
|
assert generator.detect_validation_type("build-args", {}) == "key_value_list"
|
||||||
assert generator.detect_validation_type("version", {}) == "flexible_version"
|
assert generator.detect_validation_type("version", {}) == "flexible_version"
|
||||||
assert (
|
assert (
|
||||||
generator.detect_validation_type("dotnet-version", {}) == "dotnet_version"
|
generator.detect_validation_type("dotnet-version", {}) == "dotnet_version"
|
||||||
|
|||||||
@@ -556,13 +556,33 @@ class ConventionBasedValidator(BaseValidator):
|
|||||||
self._validator_modules["codeql"] = codeql.CodeQLValidator()
|
self._validator_modules["codeql"] = codeql.CodeQLValidator()
|
||||||
return self._validator_modules["codeql"], f"validate_{validator_type}"
|
return self._validator_modules["codeql"], f"validate_{validator_type}"
|
||||||
|
|
||||||
# PHP-specific validators
|
# Convention-based validators
|
||||||
if validator_type in ["php_extensions", "coverage_driver", "mode_enum"]:
|
if validator_type in [
|
||||||
# Return self for PHP-specific validation methods
|
"php_extensions",
|
||||||
|
"coverage_driver",
|
||||||
|
"mode_enum",
|
||||||
|
"binary_enum",
|
||||||
|
"multi_value_enum",
|
||||||
|
"report_format",
|
||||||
|
"format_enum",
|
||||||
|
"linter_list",
|
||||||
|
"timeout_with_unit",
|
||||||
|
"severity_enum",
|
||||||
|
"scanner_list",
|
||||||
|
"exit_code_list",
|
||||||
|
"key_value_list",
|
||||||
|
"path_list",
|
||||||
|
"network_mode",
|
||||||
|
"language_enum",
|
||||||
|
"framework_mode",
|
||||||
|
"json_format",
|
||||||
|
"cache_config",
|
||||||
|
]:
|
||||||
|
# Return self for validation methods implemented in this class
|
||||||
return self, f"_validate_{validator_type}"
|
return self, f"_validate_{validator_type}"
|
||||||
|
|
||||||
# Package manager and report format validators
|
# Package manager validators
|
||||||
if validator_type in ["package_manager_enum", "report_format"]:
|
if validator_type in ["package_manager_enum"]:
|
||||||
# These could be in a separate module, but for now we'll put them in file validator
|
# These could be in a separate module, but for now we'll put them in file validator
|
||||||
if "file" not in self._validator_modules:
|
if "file" not in self._validator_modules:
|
||||||
from . import file
|
from . import file
|
||||||
@@ -592,9 +612,104 @@ class ConventionBasedValidator(BaseValidator):
|
|||||||
# Default range
|
# Default range
|
||||||
return 0, 100
|
return 0, 100
|
||||||
|
|
||||||
|
def _validate_comma_separated_list(
|
||||||
|
self,
|
||||||
|
value: str,
|
||||||
|
input_name: str,
|
||||||
|
item_pattern: str | None = None,
|
||||||
|
valid_items: list | None = None,
|
||||||
|
check_injection: bool = False,
|
||||||
|
item_name: str = "item",
|
||||||
|
) -> bool:
|
||||||
|
"""Validate comma-separated list of items (generic validator).
|
||||||
|
|
||||||
|
This is a generic validator that can be used for any comma-separated list
|
||||||
|
with either pattern-based or enum-based validation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The comma-separated list value
|
||||||
|
input_name: The input name for error messages
|
||||||
|
item_pattern: Regex pattern each item must match (default: alphanumeric+hyphens+underscores)
|
||||||
|
valid_items: Optional list of valid items for enum-style validation
|
||||||
|
check_injection: Whether to check for shell injection patterns
|
||||||
|
item_name: Descriptive name for items in error messages (e.g., "linter", "extension")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> # Pattern-based validation
|
||||||
|
>>> validator._validate_comma_separated_list(
|
||||||
|
... "gosec,govet", "enable-linters",
|
||||||
|
... item_pattern=r'^[a-zA-Z0-9_-]+$',
|
||||||
|
... item_name="linter"
|
||||||
|
... )
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> # Enum-based validation
|
||||||
|
>>> validator._validate_comma_separated_list(
|
||||||
|
... "vuln,config", "scanners",
|
||||||
|
... valid_items=["vuln", "config", "secret", "license"],
|
||||||
|
... item_name="scanner"
|
||||||
|
... )
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
if not value or value.strip() == "":
|
||||||
|
return True # Optional
|
||||||
|
|
||||||
|
# Security check for injection patterns
|
||||||
|
if check_injection and re.search(r"[;&|`$()]", value):
|
||||||
|
self.add_error(
|
||||||
|
f"Potential injection detected in {input_name}: {value}. "
|
||||||
|
f"Avoid using shell metacharacters (;, &, |, `, $, parentheses)"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Split by comma and validate each item
|
||||||
|
items = [item.strip() for item in value.split(",")]
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if not item: # Empty after strip
|
||||||
|
self.add_error(f"Invalid {input_name}: {value}. Contains empty {item_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Enum-based validation (if valid_items provided)
|
||||||
|
if valid_items is not None:
|
||||||
|
if item not in valid_items:
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid {item_name} '{item}' in {input_name}. "
|
||||||
|
f"Must be one of: {', '.join(valid_items)}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Pattern-based validation (if no valid_items and pattern provided)
|
||||||
|
elif item_pattern is not None:
|
||||||
|
if not re.match(item_pattern, item):
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid {item_name} '{item}' in {input_name}. "
|
||||||
|
f"Must match pattern: alphanumeric with hyphens/underscores"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Default pattern if neither valid_items nor item_pattern provided
|
||||||
|
elif not re.match(r"^[a-zA-Z0-9_-]+$", item):
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid {item_name} '{item}' in {input_name}. "
|
||||||
|
f"Must be alphanumeric with hyphens/underscores"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def _validate_php_extensions(self, value: str, input_name: str) -> bool:
|
def _validate_php_extensions(self, value: str, input_name: str) -> bool:
|
||||||
"""Validate PHP extensions format.
|
"""Validate PHP extensions format.
|
||||||
|
|
||||||
|
Wrapper for comma-separated list validator with PHP extension-specific rules.
|
||||||
|
Allows alphanumeric characters, underscores, and spaces.
|
||||||
|
Checks for shell injection patterns.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
value: The extensions value (comma-separated list)
|
value: The extensions value (comma-separated list)
|
||||||
input_name: The input name for error messages
|
input_name: The input name for error messages
|
||||||
@@ -602,59 +717,736 @@ class ConventionBasedValidator(BaseValidator):
|
|||||||
Returns:
|
Returns:
|
||||||
True if valid, False otherwise
|
True if valid, False otherwise
|
||||||
"""
|
"""
|
||||||
import re
|
return self._validate_comma_separated_list(
|
||||||
|
value,
|
||||||
|
input_name,
|
||||||
|
item_pattern=r"^[a-zA-Z0-9_\s]+$",
|
||||||
|
check_injection=True,
|
||||||
|
item_name="extension",
|
||||||
|
)
|
||||||
|
|
||||||
if not value:
|
def _validate_binary_enum(
|
||||||
return True
|
self,
|
||||||
|
value: str,
|
||||||
|
input_name: str,
|
||||||
|
valid_values: list | None = None,
|
||||||
|
case_sensitive: bool = True,
|
||||||
|
) -> bool:
|
||||||
|
"""Validate binary enum (two-value choice) (generic validator).
|
||||||
|
|
||||||
# Check for injection patterns
|
This is a generic validator for two-value enums (e.g., check/fix, enabled/disabled).
|
||||||
if re.search(r"[;&|`$()@#]", value):
|
|
||||||
self.add_error(f"Potential injection detected in {input_name}: {value}")
|
Args:
|
||||||
|
value: The enum value
|
||||||
|
input_name: The input name for error messages
|
||||||
|
valid_values: List of exactly 2 valid values (default: ["check", "fix"])
|
||||||
|
case_sensitive: Whether validation is case-sensitive (default: True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> # Default check/fix mode
|
||||||
|
>>> validator._validate_binary_enum("check", "mode")
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> # Custom binary enum
|
||||||
|
>>> validator._validate_binary_enum(
|
||||||
|
... "enabled", "status",
|
||||||
|
... valid_values=["enabled", "disabled"]
|
||||||
|
... )
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
if valid_values is None:
|
||||||
|
valid_values = ["check", "fix"]
|
||||||
|
|
||||||
|
if len(valid_values) != 2:
|
||||||
|
raise ValueError(
|
||||||
|
f"Binary enum requires exactly 2 valid values, got {len(valid_values)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not value or value.strip() == "":
|
||||||
|
return True # Optional
|
||||||
|
|
||||||
|
# Case-insensitive comparison if needed
|
||||||
|
if not case_sensitive:
|
||||||
|
value_lower = value.lower()
|
||||||
|
valid_values_lower = [v.lower() for v in valid_values]
|
||||||
|
if value_lower not in valid_values_lower:
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid {input_name}: {value}. Must be one of: {', '.join(valid_values)}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if value not in valid_values:
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid {input_name}: {value}. Must be one of: {', '.join(valid_values)}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _validate_format_enum(
|
||||||
|
self,
|
||||||
|
value: str,
|
||||||
|
input_name: str,
|
||||||
|
valid_formats: list | None = None,
|
||||||
|
allow_custom: bool = False,
|
||||||
|
) -> bool:
|
||||||
|
"""Validate output format enum (generic validator).
|
||||||
|
|
||||||
|
Generic validator for tool output formats (SARIF, JSON, XML, etc.).
|
||||||
|
Supports common formats across linting/analysis tools.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The format value
|
||||||
|
input_name: The input name for error messages
|
||||||
|
valid_formats: List of valid formats (default: comprehensive list)
|
||||||
|
allow_custom: Whether to allow formats not in the predefined list (default: False)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> # Default comprehensive format list
|
||||||
|
>>> validator._validate_format_enum("json", "format")
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> # Tool-specific format list
|
||||||
|
>>> validator._validate_format_enum(
|
||||||
|
... "sarif", "output-format",
|
||||||
|
... valid_formats=["json", "sarif", "text"]
|
||||||
|
... )
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
if valid_formats is None:
|
||||||
|
# Comprehensive list of common formats across all tools
|
||||||
|
valid_formats = [
|
||||||
|
"checkstyle",
|
||||||
|
"colored-line-number",
|
||||||
|
"compact",
|
||||||
|
"github-actions",
|
||||||
|
"html",
|
||||||
|
"json",
|
||||||
|
"junit",
|
||||||
|
"junit-xml",
|
||||||
|
"line-number",
|
||||||
|
"sarif",
|
||||||
|
"stylish",
|
||||||
|
"tab",
|
||||||
|
"teamcity",
|
||||||
|
"xml",
|
||||||
|
]
|
||||||
|
|
||||||
|
if not value or value.strip() == "":
|
||||||
|
return True # Optional
|
||||||
|
|
||||||
|
# Check if format is valid
|
||||||
|
if value not in valid_formats and not allow_custom:
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid {input_name}: {value}. Must be one of: {', '.join(valid_formats)}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check format - should be alphanumeric, underscores, commas, spaces only
|
return True
|
||||||
if not re.match(r"^[a-zA-Z0-9_,\s]+$", value):
|
|
||||||
self.add_error(f"Invalid format for {input_name}: {value}")
|
def _validate_multi_value_enum(
|
||||||
return False
|
self,
|
||||||
|
value: str,
|
||||||
|
input_name: str,
|
||||||
|
valid_values: list | None = None,
|
||||||
|
case_sensitive: bool = True,
|
||||||
|
min_values: int = 2,
|
||||||
|
max_values: int = 10,
|
||||||
|
) -> bool:
|
||||||
|
"""Validate multi-value enum (2-10 value choice) (generic validator).
|
||||||
|
|
||||||
|
Generic validator for enums with 2-10 predefined values.
|
||||||
|
For exactly 2 values, use _validate_binary_enum instead.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The enum value
|
||||||
|
input_name: The input name for error messages
|
||||||
|
valid_values: List of valid values (2-10 items required)
|
||||||
|
case_sensitive: Whether validation is case-sensitive (default: True)
|
||||||
|
min_values: Minimum number of valid values (default: 2)
|
||||||
|
max_values: Maximum number of valid values (default: 10)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> # Framework selection (3 values)
|
||||||
|
>>> validator._validate_multi_value_enum(
|
||||||
|
... "laravel", "framework",
|
||||||
|
... valid_values=["auto", "laravel", "generic"]
|
||||||
|
... )
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> # Language selection (4 values)
|
||||||
|
>>> validator._validate_multi_value_enum(
|
||||||
|
... "python", "language",
|
||||||
|
... valid_values=["php", "python", "go", "dotnet"]
|
||||||
|
... )
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
if valid_values is None:
|
||||||
|
raise ValueError("valid_values is required for multi_value_enum validator")
|
||||||
|
|
||||||
|
# Validate valid_values count
|
||||||
|
if len(valid_values) < min_values:
|
||||||
|
raise ValueError(
|
||||||
|
f"Multi-value enum requires at least {min_values} valid values, got {len(valid_values)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(valid_values) > max_values:
|
||||||
|
raise ValueError(
|
||||||
|
f"Multi-value enum supports at most {max_values} valid values, got {len(valid_values)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not value or value.strip() == "":
|
||||||
|
return True # Optional
|
||||||
|
|
||||||
|
# Case-insensitive comparison if needed
|
||||||
|
if not case_sensitive:
|
||||||
|
value_lower = value.lower()
|
||||||
|
valid_values_lower = [v.lower() for v in valid_values]
|
||||||
|
if value_lower not in valid_values_lower:
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid {input_name}: {value}. Must be one of: {', '.join(valid_values)}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if value not in valid_values:
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid {input_name}: {value}. Must be one of: {', '.join(valid_values)}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _validate_coverage_driver(self, value: str, input_name: str) -> bool:
|
def _validate_coverage_driver(self, value: str, input_name: str) -> bool:
|
||||||
"""Validate coverage driver enum.
|
"""Validate coverage driver enum.
|
||||||
|
|
||||||
|
Wrapper for multi_value_enum validator with PHP coverage driver options.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
value: The coverage driver value
|
value: The coverage driver value
|
||||||
input_name: The input name for error messages
|
input_name: The input name for error messages
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if valid, False otherwise
|
True if valid, False otherwise
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Valid: "xdebug", "pcov", "xdebug3", "none", ""
|
||||||
|
Invalid: "xdebug2", "XDEBUG", "coverage"
|
||||||
"""
|
"""
|
||||||
valid_drivers = ["none", "xdebug", "pcov", "xdebug3"]
|
return self._validate_multi_value_enum(
|
||||||
|
value,
|
||||||
if value and value not in valid_drivers:
|
input_name,
|
||||||
self.add_error(
|
valid_values=["none", "xdebug", "pcov", "xdebug3"],
|
||||||
f"Invalid {input_name}: {value}. Must be one of: {', '.join(valid_drivers)}"
|
case_sensitive=True,
|
||||||
)
|
)
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _validate_mode_enum(self, value: str, input_name: str) -> bool:
|
def _validate_mode_enum(self, value: str, input_name: str) -> bool:
|
||||||
"""Validate mode enum for linting actions.
|
"""Validate mode enum for linting actions.
|
||||||
|
|
||||||
|
Wrapper for binary_enum validator with check/fix modes.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
value: The mode value
|
value: The mode value
|
||||||
input_name: The input name for error messages
|
input_name: The input name for error messages
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if valid, False otherwise
|
True if valid, False otherwise
|
||||||
"""
|
|
||||||
valid_modes = ["check", "fix"]
|
|
||||||
|
|
||||||
if value and value not in valid_modes:
|
Examples:
|
||||||
|
Valid: "check", "fix", ""
|
||||||
|
Invalid: "invalid", "CHECK", "Fix"
|
||||||
|
"""
|
||||||
|
return self._validate_binary_enum(
|
||||||
|
value,
|
||||||
|
input_name,
|
||||||
|
valid_values=["check", "fix"],
|
||||||
|
case_sensitive=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_report_format(self, value: str, input_name: str) -> bool:
|
||||||
|
"""Validate report format for linting/analysis actions.
|
||||||
|
|
||||||
|
Wrapper for format_enum validator with comprehensive format list.
|
||||||
|
Supports multiple report formats used across different tools.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The report format value
|
||||||
|
input_name: The input name for error messages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Valid: "json", "sarif", "checkstyle", "github-actions", ""
|
||||||
|
Invalid: "invalid", "txt", "pdf"
|
||||||
|
"""
|
||||||
|
return self._validate_format_enum(value, input_name)
|
||||||
|
|
||||||
|
def _validate_linter_list(self, value: str, input_name: str) -> bool:
|
||||||
|
"""Validate comma-separated list of linter names.
|
||||||
|
|
||||||
|
Wrapper for comma-separated list validator with linter-specific rules.
|
||||||
|
Allows alphanumeric characters, hyphens, and underscores.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The linter list value
|
||||||
|
input_name: The input name for error messages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Valid: "gosec,govet,staticcheck", "errcheck"
|
||||||
|
Invalid: "gosec,,govet", "invalid linter", "linter@123"
|
||||||
|
"""
|
||||||
|
return self._validate_comma_separated_list(
|
||||||
|
value,
|
||||||
|
input_name,
|
||||||
|
item_pattern=r"^[a-zA-Z0-9_-]+$",
|
||||||
|
item_name="linter",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_timeout_with_unit(self, value: str, input_name: str) -> bool:
|
||||||
|
"""Validate timeout duration with unit (Go duration format).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The timeout value
|
||||||
|
input_name: The input name for error messages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
if not value or value.strip() == "":
|
||||||
|
return True # Optional
|
||||||
|
|
||||||
|
# Go duration format: number + unit (ns, us/µs, ms, s, m, h)
|
||||||
|
pattern = r"^[0-9]+(ns|us|µs|ms|s|m|h)$"
|
||||||
|
|
||||||
|
if not re.match(pattern, value):
|
||||||
self.add_error(
|
self.add_error(
|
||||||
f"Invalid {input_name}: {value}. Must be one of: {', '.join(valid_modes)}"
|
f"Invalid {input_name}: {value}. Expected format: number with unit "
|
||||||
|
"(e.g., 5m, 30s, 1h, 500ms)"
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _validate_severity_enum(self, value: str, input_name: str) -> bool:
|
||||||
|
"""Validate severity levels enum (generalized).
|
||||||
|
|
||||||
|
Generic validator for security tool severity levels.
|
||||||
|
Supports common severity formats used by various security tools.
|
||||||
|
|
||||||
|
Default levels: UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL (Trivy/CVSSv3 style)
|
||||||
|
Case-sensitive by default.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The severity value (comma-separated for multiple levels)
|
||||||
|
input_name: The input name for error messages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
"""
|
||||||
|
if not value or value.strip() == "":
|
||||||
|
return True # Optional
|
||||||
|
|
||||||
|
# Standard severity levels (Trivy/CVSSv3/OWASP compatible)
|
||||||
|
# Can be extended for specific tools by creating tool-specific validators
|
||||||
|
valid_severities = ["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"]
|
||||||
|
|
||||||
|
# Split by comma and validate each severity
|
||||||
|
severities = [s.strip() for s in value.split(",")]
|
||||||
|
|
||||||
|
for severity in severities:
|
||||||
|
if not severity: # Empty after strip
|
||||||
|
self.add_error(f"Invalid {input_name}: {value}. Contains empty severity level")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Case-sensitive validation
|
||||||
|
if severity not in valid_severities:
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid {input_name}: {value}. Severity '{severity}' is not valid. "
|
||||||
|
f"Must be one of: {', '.join(valid_severities)}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _validate_scanner_list(self, value: str, input_name: str) -> bool:
|
||||||
|
"""Validate comma-separated list of scanner types (for Trivy).
|
||||||
|
|
||||||
|
Wrapper for comma-separated list validator with Trivy scanner enum validation.
|
||||||
|
Supports: vuln, config, secret, license
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The scanner list value (comma-separated)
|
||||||
|
input_name: The input name for error messages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Valid: "vuln,config,secret", "vuln", "config,license"
|
||||||
|
Invalid: "invalid", "vuln,invalid,config", "vuln,,config"
|
||||||
|
"""
|
||||||
|
return self._validate_comma_separated_list(
|
||||||
|
value,
|
||||||
|
input_name,
|
||||||
|
valid_items=["vuln", "config", "secret", "license"],
|
||||||
|
item_name="scanner",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_exit_code_list(self, value: str, input_name: str) -> bool:
|
||||||
|
"""Validate comma-separated list of exit codes.
|
||||||
|
|
||||||
|
Validates Unix/Linux exit codes (0-255) in comma-separated format.
|
||||||
|
Used for retry logic, success codes, and error handling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The exit code list value (comma-separated integers)
|
||||||
|
input_name: The input name for error messages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Valid: "0", "0,1,2", "5,10,15", "0,130", ""
|
||||||
|
Invalid: "256", "0,256", "-1", "0,abc", "0,,1"
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
if not value or value.strip() == "":
|
||||||
|
return True # Optional
|
||||||
|
|
||||||
|
# Split by comma and validate each exit code
|
||||||
|
codes = [code.strip() for code in value.split(",")]
|
||||||
|
|
||||||
|
for code in codes:
|
||||||
|
if not code: # Empty after strip
|
||||||
|
self.add_error(f"Invalid {input_name}: {value}. Contains empty exit code")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if code is numeric
|
||||||
|
if not re.match(r"^[0-9]+$", code):
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid exit code '{code}' in {input_name}. "
|
||||||
|
f"Exit codes must be integers (0-255)"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Validate range (0-255 for Unix/Linux exit codes)
|
||||||
|
code_int = int(code)
|
||||||
|
if code_int < 0 or code_int > 255:
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid exit code '{code}' in {input_name}. Exit codes must be in range 0-255"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _validate_key_value_list(
|
||||||
|
self,
|
||||||
|
value: str,
|
||||||
|
input_name: str,
|
||||||
|
key_pattern: str | None = None,
|
||||||
|
check_injection: bool = True,
|
||||||
|
) -> bool:
|
||||||
|
"""Validate comma-separated list of key-value pairs (generic validator).
|
||||||
|
|
||||||
|
Validates KEY=VALUE,KEY2=VALUE2 format commonly used for Docker build-args,
|
||||||
|
environment variables, and other configuration parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The key-value list value (comma-separated KEY=VALUE pairs)
|
||||||
|
input_name: The input name for error messages
|
||||||
|
key_pattern: Regex pattern for key validation (default: alphanumeric+underscores+hyphens)
|
||||||
|
check_injection: Whether to check for shell injection patterns in values (default: True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid, False otherwise
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Valid: "KEY=value", "KEY1=value1,KEY2=value2", "BUILD_ARG=hello", ""
|
||||||
|
Invalid: "KEY", "=value", "KEY=", "KEY=value,", "KEY=val;whoami"
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
if not value or value.strip() == "":
|
||||||
|
return True # Optional
|
||||||
|
|
||||||
|
if key_pattern is None:
|
||||||
|
# Default: alphanumeric, underscores, hyphens (common for env vars and build args)
|
||||||
|
key_pattern = r"^[a-zA-Z0-9_-]+$"
|
||||||
|
|
||||||
|
# Security check for injection patterns in the entire value
|
||||||
|
if check_injection and re.search(r"[;&|`$()]", value):
|
||||||
|
self.add_error(
|
||||||
|
f"Potential injection detected in {input_name}: {value}. "
|
||||||
|
f"Avoid using shell metacharacters (;, &, |, `, $, parentheses)"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Split by comma and validate each key-value pair
|
||||||
|
pairs = [pair.strip() for pair in value.split(",")]
|
||||||
|
|
||||||
|
for pair in pairs:
|
||||||
|
if not pair: # Empty after strip
|
||||||
|
self.add_error(f"Invalid {input_name}: {value}. Contains empty key-value pair")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for KEY=VALUE format
|
||||||
|
if "=" not in pair:
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid key-value pair '{pair}' in {input_name}. Expected format: KEY=VALUE"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Split by first = only (value may contain =)
|
||||||
|
parts = pair.split("=", 1)
|
||||||
|
key = parts[0].strip()
|
||||||
|
|
||||||
|
# Validate key is not empty
|
||||||
|
if not key:
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid key-value pair '{pair}' in {input_name}. Key cannot be empty"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Validate key pattern
|
||||||
|
if not re.match(key_pattern, key):
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid key '{key}' in {input_name}. "
|
||||||
|
f"Keys must be alphanumeric with underscores/hyphens"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Note: Value can be empty (KEY=) - this is valid for some use cases
|
||||||
|
# Value validation is optional and handled by the check_injection flag above
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _validate_path_list(
|
||||||
|
self,
|
||||||
|
value: str,
|
||||||
|
input_name: str,
|
||||||
|
allow_glob: bool = True,
|
||||||
|
check_injection: bool = True,
|
||||||
|
) -> bool:
|
||||||
|
"""Validate comma-separated list of file paths or glob patterns (generic validator).
|
||||||
|
|
||||||
|
Validates file paths and glob patterns commonly used for ignore-paths,
|
||||||
|
restore-keys, file-pattern, and other path-based inputs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The path list to validate
|
||||||
|
input_name: Name of the input being validated
|
||||||
|
allow_glob: Whether to allow glob patterns (*, **, ?, [])
|
||||||
|
check_injection: Whether to check for shell injection patterns
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Valid: "*.js", "src/**/*.ts", "dist/,build/", ".github/workflows/*", ""
|
||||||
|
Invalid: "../etc/passwd", "file;rm -rf /", "path|whoami"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if valid, False otherwise
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
if not value or value.strip() == "":
|
||||||
|
return True # Optional
|
||||||
|
|
||||||
|
# Security check for injection patterns
|
||||||
|
if check_injection and re.search(r"[;&|`$()]", value):
|
||||||
|
self.add_error(
|
||||||
|
f"Potential injection detected in {input_name}: {value}. "
|
||||||
|
f"Avoid using shell metacharacters (;, &, |, `, $, parentheses)"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Split by comma and validate each path
|
||||||
|
paths = [path.strip() for path in value.split(",")]
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
if not path: # Empty after strip
|
||||||
|
self.add_error(f"Invalid {input_name}: {value}. Contains empty path")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for path traversal attempts
|
||||||
|
if "../" in path or "/.." in path or path.startswith(".."):
|
||||||
|
self.add_error(
|
||||||
|
f"Path traversal detected in {input_name}: {path}. Avoid using '..' in paths"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Validate glob patterns if allowed
|
||||||
|
if allow_glob:
|
||||||
|
# Glob patterns are valid: *, **, ?, [], {}
|
||||||
|
# Check for valid glob characters
|
||||||
|
glob_pattern = r"^[a-zA-Z0-9_\-./\*\?\[\]\{\},@~+]+$"
|
||||||
|
if not re.match(glob_pattern, path):
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid path '{path}' in {input_name}. "
|
||||||
|
f"Paths may contain alphanumeric characters, hyphens, underscores, "
|
||||||
|
f"slashes, and glob patterns (*, **, ?, [], {{}})"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# No glob patterns allowed - only alphanumeric, hyphens, underscores, slashes
|
||||||
|
path_pattern = r"^[a-zA-Z0-9_\-./,@~+]+$"
|
||||||
|
if not re.match(path_pattern, path):
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid path '{path}' in {input_name}. "
|
||||||
|
f"Paths may only contain alphanumeric characters, hyphens, "
|
||||||
|
f"underscores, and slashes"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _validate_network_mode(self, value: str, input_name: str) -> bool:
|
||||||
|
"""Validate Docker network mode enum.
|
||||||
|
|
||||||
|
Wrapper for multi_value_enum validator with Docker network mode options.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Valid: "host", "none", "default", ""
|
||||||
|
Invalid: "bridge", "NONE", "custom"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if valid, False otherwise
|
||||||
|
"""
|
||||||
|
return self._validate_multi_value_enum(
|
||||||
|
value,
|
||||||
|
input_name,
|
||||||
|
valid_values=["host", "none", "default"],
|
||||||
|
case_sensitive=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_language_enum(self, value: str, input_name: str) -> bool:
|
||||||
|
"""Validate language enum for version detection.
|
||||||
|
|
||||||
|
Wrapper for multi_value_enum validator with supported language options.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Valid: "php", "python", "go", "dotnet", ""
|
||||||
|
Invalid: "node", "ruby", "PHP"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if valid, False otherwise
|
||||||
|
"""
|
||||||
|
return self._validate_multi_value_enum(
|
||||||
|
value,
|
||||||
|
input_name,
|
||||||
|
valid_values=["php", "python", "go", "dotnet"],
|
||||||
|
case_sensitive=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_framework_mode(self, value: str, input_name: str) -> bool:
|
||||||
|
"""Validate PHP framework detection mode.
|
||||||
|
|
||||||
|
Wrapper for multi_value_enum validator with framework mode options.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Valid: "auto", "laravel", "generic", ""
|
||||||
|
Invalid: "symfony", "Auto", "LARAVEL"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if valid, False otherwise
|
||||||
|
"""
|
||||||
|
return self._validate_multi_value_enum(
|
||||||
|
value,
|
||||||
|
input_name,
|
||||||
|
valid_values=["auto", "laravel", "generic"],
|
||||||
|
case_sensitive=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_json_format(self, value: str, input_name: str) -> bool:
|
||||||
|
"""Validate JSON format string.
|
||||||
|
|
||||||
|
Validates that input is valid JSON. Used for structured configuration
|
||||||
|
data like platform-specific build arguments.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Valid: '{"key":"value"}', '[]', '{"platforms":["linux/amd64"]}', ""
|
||||||
|
Invalid: '{invalid}', 'not json', '{key:value}'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if valid, False otherwise
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
if not value or value.strip() == "":
|
||||||
|
return True # Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
json.loads(value)
|
||||||
|
return True
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
self.add_error(f"Invalid JSON format in {input_name}: {value}. Error: {str(e)}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
self.add_error(f"Failed to validate JSON in {input_name}: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _validate_cache_config(self, value: str, input_name: str) -> bool:
|
||||||
|
"""Validate Docker BuildKit cache configuration.
|
||||||
|
|
||||||
|
Validates Docker cache export/import configuration format.
|
||||||
|
Common formats: type=registry,ref=..., type=local,dest=..., type=gha
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Valid: "type=registry,ref=user/repo:cache", "type=local,dest=/tmp/cache",
|
||||||
|
"type=gha", "type=inline", ""
|
||||||
|
Invalid: "invalid", "type=", "registry", "type=unknown"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if valid, False otherwise
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
if not value or value.strip() == "":
|
||||||
|
return True # Optional
|
||||||
|
|
||||||
|
# Check basic format: type=value[,key=value,...]
|
||||||
|
if not re.match(r"^type=[a-z0-9-]+", value):
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid cache config in {input_name}: {value}. "
|
||||||
|
f"Must start with 'type=<cache-type>'"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Valid cache types
|
||||||
|
valid_types = ["registry", "local", "gha", "inline", "s3", "azblob", "oci"]
|
||||||
|
|
||||||
|
# Extract type
|
||||||
|
type_match = re.match(r"^type=([a-z0-9-]+)", value)
|
||||||
|
if type_match:
|
||||||
|
cache_type = type_match.group(1)
|
||||||
|
if cache_type not in valid_types:
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid cache type '{cache_type}' in {input_name}. "
|
||||||
|
f"Valid types: {', '.join(valid_types)}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Validate key=value pairs format
|
||||||
|
parts = value.split(",")
|
||||||
|
for part in parts:
|
||||||
|
if "=" not in part:
|
||||||
|
self.add_error(
|
||||||
|
f"Invalid cache config format in {input_name}: {value}. "
|
||||||
|
f"Each part must be in 'key=value' format"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user