Skip to content

Security

security

Security rules — Bandit + secret-pattern detection.

SecurityPatternRule dataclass

Bases: ProjectRule

Detect hardcoded secrets via regex patterns.

Source code in packages/axm-audit/src/axm_audit/core/rules/security.py
Python
@dataclass
@register_rule("security")
class SecurityPatternRule(ProjectRule):
    """Detect hardcoded secrets via regex patterns."""

    patterns: list[str] = field(
        default_factory=lambda: [
            r"password\s*=\s*[\"'][^\"']+[\"']",
            r"secret\s*=\s*[\"'][^\"']+[\"']",
            r"api_key\s*=\s*[\"'][^\"']+[\"']",
            r"token\s*=\s*[\"'][^\"']+[\"']",
        ]
    )

    @property
    def rule_id(self) -> str:
        """Unique identifier for this rule."""
        return "PRACTICE_SECURITY"

    def _scan_file_for_secrets(
        self, path: Path, src_path: Path
    ) -> list[dict[str, str | int]]:
        try:
            content = path.read_text()
        except (OSError, UnicodeDecodeError):
            return []

        found: list[dict[str, str | int]] = []
        for pattern in self.patterns:
            for match in re.finditer(pattern, content, re.IGNORECASE):
                line_num = content[: match.start()].count("\n") + 1
                found.append(
                    {
                        "file": str(path.relative_to(src_path)),
                        "line": line_num,
                        "pattern": pattern.split(r"\s*")[0],
                    }
                )
        return found

    def _build_secret_result(self, matches: list[dict[str, str | int]]) -> CheckResult:
        count = len(matches)
        passed = count == 0
        score = max(0, 100 - count * 25)
        text_lines = [f"• {m['file']}:{m['line']} {m['pattern']}" for m in matches]

        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"{count} potential secret(s) found",
            severity=Severity.ERROR if not passed else Severity.INFO,
            score=int(score),
            details={"secret_count": count, "matches": matches},
            text="\n".join(text_lines) if text_lines else None,
            fix_hint="Use environment variables or secret managers"
            if not passed
            else None,
        )

    def check(self, project_path: Path) -> CheckResult:
        """Check for hardcoded secrets in the project."""
        early = self.check_src(project_path)
        if early is not None:
            return early

        src_path = project_path / "src"
        matches: list[dict[str, str | int]] = []
        for path in get_python_files(src_path):
            matches.extend(self._scan_file_for_secrets(path, src_path))

        return self._build_secret_result(matches)
rule_id property

Unique identifier for this rule.

check(project_path)

Check for hardcoded secrets in the project.

Source code in packages/axm-audit/src/axm_audit/core/rules/security.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Check for hardcoded secrets in the project."""
    early = self.check_src(project_path)
    if early is not None:
        return early

    src_path = project_path / "src"
    matches: list[dict[str, str | int]] = []
    for path in get_python_files(src_path):
        matches.extend(self._scan_file_for_secrets(path, src_path))

    return self._build_secret_result(matches)

SecurityRule dataclass

Bases: ProjectRule

Run Bandit and score based on vulnerability severity.

Scoring: 100 - (high_count * 15 + medium_count * 5), min 0.

Source code in packages/axm-audit/src/axm_audit/core/rules/security.py
Python
@dataclass
@register_rule("security")
class SecurityRule(ProjectRule):
    """Run Bandit and score based on vulnerability severity.

    Scoring: 100 - (high_count * 15 + medium_count * 5), min 0.
    """

    @property
    def rule_id(self) -> str:
        """Unique identifier for this rule."""
        return "QUALITY_SECURITY"

    def check(self, project_path: Path) -> CheckResult:
        """Check project security with Bandit."""
        early = self.check_src(project_path)
        if early is not None:
            return early

        src_path = project_path / "src"

        try:
            data = run_bandit(src_path, project_path)
        except FileNotFoundError:
            return CheckResult(
                rule_id=self.rule_id,
                passed=False,
                message="bandit not available",
                severity=Severity.ERROR,
                score=0,
                details={"high_count": 0, "medium_count": 0},
                fix_hint="Install with: uv add --dev bandit",
            )
        except RuntimeError as exc:
            return CheckResult(
                rule_id=self.rule_id,
                passed=False,
                message=str(exc),
                severity=Severity.ERROR,
                score=0,
                details={"high_count": 0, "medium_count": 0},
                fix_hint="Check bandit installation: uv run bandit --version",
            )

        raw_results = data.get("results", [])
        results: list[dict[str, object]] = (
            [r for r in raw_results if isinstance(r, dict)]
            if isinstance(raw_results, list)
            else []
        )
        return _build_security_result(self.rule_id, results)
rule_id property

Unique identifier for this rule.

check(project_path)

Check project security with Bandit.

Source code in packages/axm-audit/src/axm_audit/core/rules/security.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Check project security with Bandit."""
    early = self.check_src(project_path)
    if early is not None:
        return early

    src_path = project_path / "src"

    try:
        data = run_bandit(src_path, project_path)
    except FileNotFoundError:
        return CheckResult(
            rule_id=self.rule_id,
            passed=False,
            message="bandit not available",
            severity=Severity.ERROR,
            score=0,
            details={"high_count": 0, "medium_count": 0},
            fix_hint="Install with: uv add --dev bandit",
        )
    except RuntimeError as exc:
        return CheckResult(
            rule_id=self.rule_id,
            passed=False,
            message=str(exc),
            severity=Severity.ERROR,
            score=0,
            details={"high_count": 0, "medium_count": 0},
            fix_hint="Check bandit installation: uv run bandit --version",
        )

    raw_results = data.get("results", [])
    results: list[dict[str, object]] = (
        [r for r in raw_results if isinstance(r, dict)]
        if isinstance(raw_results, list)
        else []
    )
    return _build_security_result(self.rule_id, results)

run_bandit(src_path, project_path)

Run Bandit and return parsed JSON output.

Raises:

Type Description
RuntimeError

If bandit exits with rc >= 2 (error) and produces no parseable output. rc=0 means clean, rc=1 means issues found.

Source code in packages/axm-audit/src/axm_audit/core/rules/security.py
Python
def run_bandit(src_path: Path, project_path: Path) -> dict[str, object]:
    """Run Bandit and return parsed JSON output.

    Raises:
        RuntimeError: If bandit exits with rc >= 2 (error) and produces
            no parseable output.  rc=0 means clean, rc=1 means issues found.
    """
    result = run_in_project(
        ["bandit", "-r", "-f", "json", str(src_path)],
        project_path,
        with_packages=["bandit"],
        capture_output=True,
        check=False,
        text=True,
    )
    try:
        if result.stdout.strip():
            data: dict[str, object] = json.loads(result.stdout)
            return data
    except json.JSONDecodeError:
        pass

    if result.returncode >= _BANDIT_ERROR_RC:
        stderr = result.stderr.strip() if result.stderr else "unknown error"
        msg = f"bandit failed (rc={result.returncode}): {stderr}"
        raise RuntimeError(msg)

    if result.returncode == _BANDIT_ISSUES_RC:
        stderr = (result.stderr or "").strip()[:500]
        logger.warning(
            "QUALITY_SECURITY: bandit returned rc=1 with empty stdout "
            "(stderr: %s) — treating as no issues found, but this may "
            "indicate a silent failure",
            stderr or "<empty>",
        )

    return {}