devsecops

Securing GitHub Actions Workflows

This skill covers hardening GitHub Actions workflows against supply chain attacks, credential theft, and privilege escalation. It addresses pinning actions to SHA digests, minimizing GITHUB_TOKEN permissions, protecting secrets from exfiltration, preventing script injection in workflow expressions, and implementing required reviewers for workflow changes.

cicddevsecopsgithub-actionssecure-sdlcsupply-chainworkflow-security
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When GitHub Actions is the CI/CD platform and workflows need hardening against supply chain attacks
  • When workflows handle secrets, deploy to production, or have elevated permissions
  • When preventing script injection via untrusted PR titles, branch names, or commit messages
  • When requiring audit trails and approval gates for workflow modifications
  • When third-party actions pose supply chain risk through mutable version tags

Do not use for securing other CI/CD platforms (see platform-specific hardening guides), for application vulnerability scanning (use SAST/DAST), or for secret detection in code (use Gitleaks).

Prerequisites

  • GitHub repository with GitHub Actions enabled
  • GitHub organization admin access for organization-level settings
  • Understanding of GitHub Actions workflow syntax and events

Workflow

Step 1: Pin Actions to SHA Digests

# INSECURE: Mutable tag can be overwritten by attacker
- uses: actions/checkout@v4
 
# SECURE: Pinned to immutable SHA digest
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
 
# Use Dependabot to auto-update pinned SHAs
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    commit-message:
      prefix: "ci"

Step 2: Minimize GITHUB_TOKEN Permissions

# Set restrictive default permissions at workflow level
name: CI Pipeline
permissions: {}  # Start with no permissions
 
on: [push, pull_request]
 
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read  # Only what's needed
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
 
  deploy:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    permissions:
      contents: read
      deployments: write
      id-token: write  # For OIDC-based cloud auth
    steps:
      - name: Deploy
        run: echo "deploying"

Step 3: Prevent Script Injection

# VULNERABLE: User-controlled input in run step
- run: echo "PR title is ${{ github.event.pull_request.title }}"
 
# SECURE: Use environment variable (properly escaped by shell)
- name: Process PR
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
    PR_BODY: ${{ github.event.pull_request.body }}
  run: |
    echo "PR title is ${PR_TITLE}"
    echo "PR body is ${PR_BODY}"
 
# SECURE: Use actions/github-script for complex operations
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
  with:
    script: |
      const title = context.payload.pull_request.title;
      console.log(`PR title: ${title}`);

Step 4: Secure Fork Pull Request Handling

# DANGEROUS: pull_request_target runs with base repo permissions
# on: pull_request_target  # AVOID unless absolutely necessary
 
# SAFE: pull_request runs in fork context with limited permissions
on:
  pull_request:
    branches: [main]
 
# If pull_request_target is required, never checkout PR code:
on:
  pull_request_target:
    types: [labeled]
 
jobs:
  safe-job:
    if: contains(github.event.pull_request.labels.*.name, 'safe-to-test')
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      # NEVER do: actions/checkout with ref: ${{ github.event.pull_request.head.sha }}
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
        # This checks out the BASE branch, not the PR

Step 5: Protect Secrets and Environment Variables

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production  # Requires approval
    steps:
      - name: Deploy with secret
        env:
          # Secrets are masked in logs automatically
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          # Never echo secrets
          # echo "$DEPLOY_KEY"  # BAD
          deploy-tool --key-file <(echo "$DEPLOY_KEY")
 
      - name: Audit secret access
        run: |
          # Log that secret was used without exposing it
          echo "::notice::Deploy key accessed for production deployment"

Step 6: Implement Workflow Change Controls

# Require CODEOWNERS approval for workflow changes
# .github/CODEOWNERS
.github/workflows/ @security-team @platform-team
.github/actions/ @security-team @platform-team
 
# Organization settings:
# 1. Settings > Actions > General > Fork PR policies
#    - Require approval for first-time contributors
#    - Require approval for all outside collaborators
# 2. Settings > Actions > General > Workflow permissions
#    - Read repository contents and packages permissions
#    - Do NOT allow GitHub Actions to create and approve PRs

Key Concepts

Term Definition
SHA Pinning Referencing GitHub Actions by their immutable commit SHA instead of mutable version tags
Script Injection Attack where untrusted input (PR title, branch name) is interpolated into shell commands
GITHUB_TOKEN Automatically generated token with configurable permissions scoped to the current repository
pull_request_target Dangerous event trigger that runs in the base repo context with full permissions on fork PRs
Environment Protection GitHub feature requiring manual approval before jobs accessing an environment can run
CODEOWNERS File defining required reviewers for specific paths including workflow files
OIDC Federation Using GitHub's OIDC token to authenticate to cloud providers without storing long-lived credentials

Tools & Systems

  • Dependabot: Automated dependency updater that keeps pinned action SHAs current
  • StepSecurity Harden Runner: GitHub Action that monitors and restricts outbound network calls from workflows
  • actionlint: Linter for GitHub Actions workflow files that detects security issues
  • allstar: GitHub App by OpenSSF that enforces security policies on repositories
  • scorecard: OpenSSF tool that evaluates supply chain security practices including CI/CD

Common Scenarios

Scenario: Preventing Supply Chain Attack via Compromised Third-Party Action

Context: A widely-used GitHub Action is compromised and its v3 tag is updated to include credential-stealing code. Repositories using @v3 automatically pull the malicious version.

Approach:

  1. Pin all actions to SHA digests immediately across all repositories
  2. Configure Dependabot for github-actions ecosystem to manage SHA updates
  3. Restrict GITHUB_TOKEN permissions so even compromised actions have minimal access
  4. Add StepSecurity harden-runner to detect anomalous outbound network calls
  5. Review all third-party actions and replace unnecessary ones with inline scripts
  6. Require CODEOWNERS approval for any changes to .github/workflows/

Pitfalls: SHA pinning without Dependabot means missing legitimate security updates to actions. Overly restrictive permissions can break legitimate workflows. Using pull_request_target for label-based gating still exposes secrets if the workflow checks out PR code.

Output Format

GitHub Actions Security Audit
================================
Repository: org/web-application
Date: 2026-02-23
 
WORKFLOW ANALYSIS:
  Total workflows: 8
  Total action references: 34
 
SHA PINNING:
  [FAIL] 12/34 actions use mutable tags instead of SHA digests
  - .github/workflows/ci.yml: actions/setup-node@v4
  - .github/workflows/deploy.yml: aws-actions/configure-aws-credentials@v4
 
PERMISSIONS:
  [FAIL] 3/8 workflows have no explicit permissions (inherit default)
  [WARN] 1/8 workflows request write-all permissions
 
SCRIPT INJECTION:
  [FAIL] 2 workflow steps interpolate user input directly
  - .github/workflows/pr-check.yml:23: ${{ github.event.pull_request.title }}
 
SECRETS:
  [PASS] No secrets exposed in workflow logs
  [PASS] All production deployments use environment protection
 
SCORE: 6/10 (Remediate 5 HIGH findings)
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md1.8 KB

API Reference: Securing GitHub Actions Workflows

Security Checks

Check Risk Severity
Unpinned actions (mutable tags) Supply chain attack via tag overwrite Medium
Missing permissions block Inherits overly broad defaults Medium
write-all permissions Excessive token scope High
Script injection in run steps Code execution via PR title/body High
pull_request_target trigger Fork code runs with base permissions High
Secrets in workflow logs Credential exposure Critical

Dangerous Expression Contexts

Context Risk
github.event.pull_request.title Attacker-controlled PR title
github.event.pull_request.body Attacker-controlled PR body
github.event.issue.title Attacker-controlled issue title
github.event.comment.body Attacker-controlled comment
github.head_ref Attacker-controlled branch name

SHA Pinning Format

Format Security
actions/checkout@v4 Insecure - mutable tag
actions/checkout@b4ffde65f... Secure - immutable SHA

Permission Scopes

Scope Values
contents read, write
actions read, write
deployments read, write
id-token write (for OIDC)
security-events write
pull-requests read, write

Python Libraries

Library Version Purpose
yaml PyYAML >=6.0 Parse workflow YAML
re stdlib Pattern matching
json stdlib Report output
pathlib stdlib File discovery

References

standards.md1.1 KB

Standards Reference: Securing GitHub Actions

NIST SSDF (SP 800-218)

PS.1: Protect All Forms of Code

  • Workflows are code and must be reviewed and protected
  • Pin action dependencies to SHA digests
  • Minimize GITHUB_TOKEN permissions

CIS Software Supply Chain Security

  • BD-1: Define security requirements for build processes
  • BD-2: Automate security validation of build configurations
  • BD-3: Pin all external dependencies to immutable references

OWASP CI/CD Top 10 Risks

Risk GitHub Actions Mitigation
CICD-SEC-1: Insufficient Flow Control Environment protection rules, CODEOWNERS
CICD-SEC-3: Dependency Chain Abuse SHA pinning of actions
CICD-SEC-4: Poisoned Pipeline Execution Restrict pull_request_target, input sanitization
CICD-SEC-6: Credential Hygiene OIDC federation, minimal GITHUB_TOKEN scope
CICD-SEC-9: Artifact Integrity Sign artifacts in workflows

SLSA Framework

  • Level 2: Hosted build service (GitHub Actions qualifies)
  • Level 3: Hardened build platform with isolation guarantees
  • Workflow hardening prevents provenance falsification
workflows.md1.2 KB

Workflow Reference: Securing GitHub Actions

Hardening Checklist

  1. Pin all actions to SHA digests
  2. Set restrictive default permissions
  3. Sanitize all user-controlled inputs
  4. Never use pull_request_target with PR checkout
  5. Enable environment protection for production
  6. Configure CODEOWNERS for workflow files
  7. Enable Dependabot for github-actions
  8. Audit third-party actions quarterly
  9. Use OIDC instead of long-lived cloud credentials
  10. Add harden-runner for network monitoring

Permission Scoping Reference

Permission Use Case
contents: read Checkout code
contents: write Create releases, push tags
security-events: write Upload SARIF results
packages: write Push container images
deployments: write Create deployment status
id-token: write OIDC cloud authentication
pull-requests: write Comment on PRs

Script Injection Prevention

# DANGEROUS patterns to avoid:
run: echo "${{ github.event.issue.title }}"
run: echo "${{ github.event.comment.body }}"
run: echo "${{ github.head_ref }}"
 
# SAFE alternatives:
env:
  TITLE: ${{ github.event.issue.title }}
run: echo "${TITLE}"

Scripts 2

agent.py8.1 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for securing GitHub Actions workflows.

Audits GitHub Actions workflow files for security issues including
unpinned actions, excessive permissions, script injection risks,
dangerous triggers, and missing secret protections.
"""

import json
import re
import sys
from pathlib import Path
from datetime import datetime

try:
    import yaml
except ImportError:
    yaml = None


class GitHubActionsSecurityAgent:
    """Audits GitHub Actions workflows for security vulnerabilities."""

    def __init__(self, repo_path=".", output_dir="./gha_audit"):
        self.repo_path = Path(repo_path)
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.findings = []

    def _load_workflow(self, path):
        if yaml:
            with open(path) as f:
                return yaml.safe_load(f)
        with open(path) as f:
            return {"raw": f.read()}

    def find_workflows(self):
        """Discover all workflow files in the repository."""
        wf_dir = self.repo_path / ".github" / "workflows"
        if not wf_dir.exists():
            return []
        return sorted(wf_dir.glob("*.yml")) + sorted(wf_dir.glob("*.yaml"))

    def check_sha_pinning(self, workflow_path, content):
        """Check if actions are pinned to SHA digests."""
        unpinned = []
        raw = content.get("raw", "") if "raw" in content else ""
        if not raw:
            try:
                raw = Path(workflow_path).read_text()
            except Exception:
                return unpinned

        for line_num, line in enumerate(raw.splitlines(), 1):
            m = re.search(r'uses:\s+([^@\s]+)@([^\s#]+)', line)
            if m:
                action, ref = m.group(1), m.group(2)
                if not re.match(r'^[a-f0-9]{40}$', ref):
                    unpinned.append({
                        "action": action,
                        "ref": ref,
                        "line": line_num,
                        "file": str(workflow_path),
                    })
                    self.findings.append({
                        "severity": "medium",
                        "type": "Unpinned Action",
                        "detail": f"{action}@{ref} at line {line_num}",
                        "file": str(workflow_path),
                    })
        return unpinned

    def check_permissions(self, workflow_path, content):
        """Check for overly permissive GITHUB_TOKEN permissions."""
        issues = []
        if not isinstance(content, dict) or "raw" in content:
            return issues

        top_perms = content.get("permissions")
        if top_perms is None:
            issues.append({
                "issue": "No top-level permissions defined (inherits defaults)",
                "file": str(workflow_path),
            })
            self.findings.append({
                "severity": "medium",
                "type": "Missing Permissions",
                "detail": "Workflow has no permissions block",
                "file": str(workflow_path),
            })

        if top_perms == "write-all" or (isinstance(top_perms, dict) and
                top_perms.get("contents") == "write" and
                top_perms.get("actions") == "write"):
            issues.append({"issue": "Overly permissive write-all", "file": str(workflow_path)})
            self.findings.append({
                "severity": "high",
                "type": "Excessive Permissions",
                "detail": "write-all permissions granted",
                "file": str(workflow_path),
            })

        return issues

    def check_script_injection(self, workflow_path, content):
        """Check for user-controlled input in run steps (script injection)."""
        injections = []
        raw = content.get("raw", "") if "raw" in content else ""
        if not raw:
            try:
                raw = Path(workflow_path).read_text()
            except Exception:
                return injections

        dangerous_contexts = [
            "github.event.pull_request.title",
            "github.event.pull_request.body",
            "github.event.issue.title",
            "github.event.issue.body",
            "github.event.comment.body",
            "github.event.review.body",
            "github.head_ref",
        ]

        in_run = False
        for line_num, line in enumerate(raw.splitlines(), 1):
            stripped = line.strip()
            if stripped.startswith("run:") or stripped.startswith("run: |"):
                in_run = True
            elif in_run and not stripped.startswith("-") and not stripped.startswith("#"):
                for ctx in dangerous_contexts:
                    if f"${{{{ {ctx}" in line or f"${{{{{ctx}" in line:
                        injections.append({
                            "context": ctx,
                            "line": line_num,
                            "file": str(workflow_path),
                        })
                        self.findings.append({
                            "severity": "high",
                            "type": "Script Injection",
                            "detail": f"{ctx} in run step at line {line_num}",
                            "file": str(workflow_path),
                        })
            if stripped and not stripped.startswith("-") and not stripped.startswith("#") and ":" in stripped and not stripped.startswith("run"):
                in_run = False

        return injections

    def check_dangerous_triggers(self, workflow_path, content):
        """Check for dangerous event triggers."""
        issues = []
        if not isinstance(content, dict) or "raw" in content:
            raw = content.get("raw", "")
            if "pull_request_target" in raw:
                issues.append({"trigger": "pull_request_target", "file": str(workflow_path)})
                self.findings.append({
                    "severity": "high",
                    "type": "Dangerous Trigger",
                    "detail": "pull_request_target allows fork code to run with base permissions",
                    "file": str(workflow_path),
                })
            return issues

        on_block = content.get("on", content.get(True, {}))
        if isinstance(on_block, dict) and "pull_request_target" in on_block:
            issues.append({"trigger": "pull_request_target", "file": str(workflow_path)})
            self.findings.append({
                "severity": "high",
                "type": "Dangerous Trigger",
                "detail": "pull_request_target trigger used",
                "file": str(workflow_path),
            })
        return issues

    def audit_all(self):
        """Run all security checks on all workflow files."""
        workflows = self.find_workflows()
        results = []
        for wf in workflows:
            content = self._load_workflow(wf)
            unpinned = self.check_sha_pinning(wf, content)
            perms = self.check_permissions(wf, content)
            injections = self.check_script_injection(wf, content)
            triggers = self.check_dangerous_triggers(wf, content)
            results.append({
                "workflow": str(wf),
                "unpinned_actions": len(unpinned),
                "permission_issues": len(perms),
                "script_injections": len(injections),
                "dangerous_triggers": len(triggers),
            })
        return results

    def generate_report(self):
        audit = self.audit_all()
        report = {
            "report_date": datetime.utcnow().isoformat(),
            "repository": str(self.repo_path),
            "workflows_scanned": len(audit),
            "audit_summary": audit,
            "findings": self.findings,
            "total_findings": len(self.findings),
        }
        out = self.output_dir / "gha_security_report.json"
        with open(out, "w") as f:
            json.dump(report, f, indent=2)
        print(json.dumps(report, indent=2))
        return report


def main():
    repo = sys.argv[1] if len(sys.argv) > 1 else "."
    agent = GitHubActionsSecurityAgent(repo)
    agent.generate_report()


if __name__ == "__main__":
    main()
process.py7.4 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
GitHub Actions Workflow Security Audit Script

Analyzes workflow files for security issues including unpinned actions,
excessive permissions, script injection risks, and insecure patterns.

Usage:
    python process.py --workflows-dir .github/workflows/ --output audit-report.json
"""

import argparse
import json
import os
import re
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path

import yaml


@dataclass
class SecurityFinding:
    file: str
    line: int
    check: str
    severity: str
    message: str
    remediation: str


SHA_PATTERN = re.compile(r"@[0-9a-f]{40}")
TAG_PATTERN = re.compile(r"@v?\d+(\.\d+)*$")
INJECTION_PATTERN = re.compile(r"\$\{\{\s*github\.event\.(issue|pull_request|comment|review)\.\w+")
DANGEROUS_CONTEXTS = [
    "github.event.issue.title",
    "github.event.issue.body",
    "github.event.pull_request.title",
    "github.event.pull_request.body",
    "github.event.comment.body",
    "github.event.review.body",
    "github.head_ref",
]


def load_workflow(filepath: str) -> dict:
    """Load a GitHub Actions workflow YAML file."""
    try:
        with open(filepath, "r") as f:
            return yaml.safe_load(f) or {}
    except (yaml.YAMLError, FileNotFoundError):
        return {}


def check_action_pinning(workflow: dict, filepath: str) -> list:
    """Check if actions are pinned to SHA digests."""
    findings = []
    filename = os.path.basename(filepath)

    for job_name, job in workflow.get("jobs", {}).items():
        for i, step in enumerate(job.get("steps", [])):
            uses = step.get("uses", "")
            if not uses or uses.startswith("./"):
                continue
            if not SHA_PATTERN.search(uses):
                findings.append(SecurityFinding(
                    file=filename, line=0,
                    check="ACTION_PINNING",
                    severity="HIGH",
                    message=f"Job '{job_name}' step {i}: '{uses}' not pinned to SHA digest",
                    remediation=f"Pin to SHA: {uses.split('@')[0]}@<commit-sha>"
                ))
    return findings


def check_permissions(workflow: dict, filepath: str) -> list:
    """Check for overly permissive GITHUB_TOKEN permissions."""
    findings = []
    filename = os.path.basename(filepath)

    top_perms = workflow.get("permissions")
    if top_perms is None:
        findings.append(SecurityFinding(
            file=filename, line=0,
            check="PERMISSIONS",
            severity="MEDIUM",
            message="No top-level permissions defined. Inherits default (may be write-all).",
            remediation="Add 'permissions: {}' at workflow level and grant per-job."
        ))
    elif top_perms == "write-all" or (isinstance(top_perms, dict) and
                                       all(v == "write" for v in top_perms.values())):
        findings.append(SecurityFinding(
            file=filename, line=0,
            check="PERMISSIONS",
            severity="HIGH",
            message="Workflow has write-all permissions.",
            remediation="Restrict to minimum required permissions per job."
        ))
    return findings


def check_script_injection(workflow: dict, filepath: str) -> list:
    """Check for script injection via user-controlled inputs."""
    findings = []
    filename = os.path.basename(filepath)

    for job_name, job in workflow.get("jobs", {}).items():
        for i, step in enumerate(job.get("steps", [])):
            run_cmd = step.get("run", "")
            if not run_cmd:
                continue
            for ctx in DANGEROUS_CONTEXTS:
                if f"${{{{ {ctx}" in run_cmd or f"${{{{{ctx}" in run_cmd:
                    findings.append(SecurityFinding(
                        file=filename, line=0,
                        check="SCRIPT_INJECTION",
                        severity="CRITICAL",
                        message=f"Job '{job_name}' step {i}: '{ctx}' interpolated in run step",
                        remediation="Use env variable: env: VAR: ${{ " + ctx + " }} then ${VAR}"
                    ))
    return findings


def check_pr_target(workflow: dict, filepath: str) -> list:
    """Check for dangerous pull_request_target usage."""
    findings = []
    filename = os.path.basename(filepath)

    triggers = workflow.get("on", {})
    if isinstance(triggers, dict) and "pull_request_target" in triggers:
        for job_name, job in workflow.get("jobs", {}).items():
            for step in job.get("steps", []):
                uses = step.get("uses", "")
                if "checkout" in uses:
                    with_ref = step.get("with", {}).get("ref", "")
                    if "pull_request" in with_ref or "head" in with_ref:
                        findings.append(SecurityFinding(
                            file=filename, line=0,
                            check="PR_TARGET_CHECKOUT",
                            severity="CRITICAL",
                            message=f"Job '{job_name}': pull_request_target with PR code checkout",
                            remediation="Never checkout PR code in pull_request_target workflows."
                        ))
    return findings


def main():
    parser = argparse.ArgumentParser(description="GitHub Actions Security Audit")
    parser.add_argument("--workflows-dir", required=True)
    parser.add_argument("--output", default="actions-security-report.json")
    parser.add_argument("--fail-on-findings", action="store_true")
    args = parser.parse_args()

    workflows_dir = os.path.abspath(args.workflows_dir)
    all_findings = []

    workflow_files = list(Path(workflows_dir).glob("*.yml")) + list(Path(workflows_dir).glob("*.yaml"))
    print(f"[*] Auditing {len(workflow_files)} workflow files in {workflows_dir}")

    for wf_path in workflow_files:
        workflow = load_workflow(str(wf_path))
        if not workflow:
            continue

        all_findings.extend(check_action_pinning(workflow, str(wf_path)))
        all_findings.extend(check_permissions(workflow, str(wf_path)))
        all_findings.extend(check_script_injection(workflow, str(wf_path)))
        all_findings.extend(check_pr_target(workflow, str(wf_path)))

    severity_counts = {}
    for f in all_findings:
        severity_counts[f.severity] = severity_counts.get(f.severity, 0) + 1

    report = {
        "metadata": {
            "directory": workflows_dir,
            "date": datetime.now(timezone.utc).isoformat(),
            "workflows_scanned": len(workflow_files)
        },
        "summary": {
            "total_findings": len(all_findings),
            "severity_counts": severity_counts
        },
        "findings": [
            {"file": f.file, "check": f.check, "severity": f.severity,
             "message": f.message, "remediation": f.remediation}
            for f in sorted(all_findings,
                            key=lambda x: {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}.get(x.severity, 4))
        ]
    }

    output_path = os.path.abspath(args.output)
    with open(output_path, "w") as f:
        json.dump(report, f, indent=2)
    print(f"[*] Report: {output_path}")

    for f in all_findings:
        print(f"  [{f.severity}] {f.file}: {f.message}")

    passed = len(all_findings) == 0
    print(f"\n[{'PASS' if passed else 'FAIL'}] {len(all_findings)} security findings")

    if args.fail_on_findings and not passed:
        sys.exit(1)


if __name__ == "__main__":
    main()

Assets 1

template.mdtext/markdown · 1.0 KB
Keep exploring