Skip to content

Security

security

Node security rules — dependency vulnerabilities (npm audit) and secrets.

Ports the intent of the Python DEPS_AUDIT (vulnerable packages) and PRACTICE_SECURITY (hardcoded secrets) rules to the Node ecosystem.

Research note (false-green): npm audit --audit-level is an exit-code gate — the JSON still lists every vuln — whereas pnpm audit --audit-level is a JSON filter. We never pass --audit-level and score metadata directly. npm audit exits 1 when vulns exist (a finding, not an env failure).

NodeSecretsRule

Bases: NodeToolRule

Score hardcoded-secret findings from gitleaks.

Mirrors the Python PRACTICE_SECURITY (secret scan): 100 - secrets*25. gitleaks is a system tool (not a node_modules binary); it writes its JSON report to stdout and exits non-zero when leaks are found.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/security.py
Python
@register_rule("security", framework=Framework.NODE)
class NodeSecretsRule(NodeToolRule):
    """Score hardcoded-secret findings from gitleaks.

    Mirrors the Python ``PRACTICE_SECURITY`` (secret scan): ``100 - secrets*25``.
    gitleaks is a system tool (not a node_modules binary); it writes its JSON
    report to stdout and exits non-zero when leaks are found.
    """

    binary = "gitleaks"
    on_path = True
    install_hint = "Install gitleaks: brew install gitleaks"

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python secret-scan rule)."""
        return "PRACTICE_SECURITY"

    @property
    def args(self) -> list[str]:
        """Scan the directory, emitting the JSON report to stdout."""
        return ["dir", ".", "--report-format", "json", "--report-path", "/dev/stdout"]

    @property
    def findings_returncodes(self) -> frozenset[int]:
        """gitleaks exits 1 when leaks are found — a finding, not a crash."""
        return frozenset({1})

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by the number of secret findings (gitleaks JSON is an array)."""
        secret_count = len(parsed) if isinstance(parsed, list) else 0
        score = max(0, 100 - secret_count * 25)
        passed = secret_count == 0
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Secrets: {secret_count} hardcoded secret(s) found",
            severity=Severity.ERROR if not passed else Severity.INFO,
            score=score,
            details={"secret_count": secret_count},
            fix_hint="Remove/rotate the leaked secrets above" if secret_count else None,
        )
args property

Scan the directory, emitting the JSON report to stdout.

findings_returncodes property

gitleaks exits 1 when leaks are found — a finding, not a crash.

rule_id property

Unique identifier (shared with the Python secret-scan rule).

score_output(parsed, project_path)

Score by the number of secret findings (gitleaks JSON is an array).

Source code in packages/axm-audit/src/axm_audit/core/rules/node/security.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by the number of secret findings (gitleaks JSON is an array)."""
    secret_count = len(parsed) if isinstance(parsed, list) else 0
    score = max(0, 100 - secret_count * 25)
    passed = secret_count == 0
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Secrets: {secret_count} hardcoded secret(s) found",
        severity=Severity.ERROR if not passed else Severity.INFO,
        score=score,
        details={"secret_count": secret_count},
        fix_hint="Remove/rotate the leaked secrets above" if secret_count else None,
    )

NodeVulnerabilityRule

Bases: NodeToolRule

Score npm-audit vulnerabilities (HIGH/CRITICAL).

Mirrors the Python DEPS_AUDIT: 100 - (high+critical) * 15. Lives in the deps category like its Python counterpart.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/security.py
Python
@register_rule("deps", framework=Framework.NODE)
class NodeVulnerabilityRule(NodeToolRule):
    """Score npm-audit vulnerabilities (HIGH/CRITICAL).

    Mirrors the Python ``DEPS_AUDIT``: ``100 - (high+critical) * 15``. Lives in
    the ``deps`` category like its Python counterpart.
    """

    binary = "npm"
    on_path = True
    install_hint = "npm is required to run `npm audit`"

    @property
    def rule_id(self) -> str:
        """Unique identifier (shared with the Python dependency-audit rule)."""
        return "DEPS_AUDIT"

    @property
    def args(self) -> list[str]:
        """Full vulnerability report as JSON (no --audit-level: that's a gate)."""
        return ["audit", "--json"]

    @property
    def findings_returncodes(self) -> frozenset[int]:
        """npm audit exits 1 when vulnerabilities are present — a finding."""
        return frozenset({1})

    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Score by HIGH (15 each) + CRITICAL (15 each) vulnerability counts."""
        high, critical = _vuln_counts(parsed)
        total = high + critical
        score = max(0, 100 - total * 15)
        passed = total == 0
        return CheckResult(
            rule_id=self.rule_id,
            passed=passed,
            message=f"Vulnerabilities: {critical} critical, {high} high",
            severity=Severity.ERROR if not passed else Severity.INFO,
            score=score,
            details={"high": high, "critical": critical},
            fix_hint="Run: npm audit fix" if total else None,
        )
args property

Full vulnerability report as JSON (no --audit-level: that's a gate).

findings_returncodes property

npm audit exits 1 when vulnerabilities are present — a finding.

rule_id property

Unique identifier (shared with the Python dependency-audit rule).

score_output(parsed, project_path)

Score by HIGH (15 each) + CRITICAL (15 each) vulnerability counts.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/security.py
Python
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Score by HIGH (15 each) + CRITICAL (15 each) vulnerability counts."""
    high, critical = _vuln_counts(parsed)
    total = high + critical
    score = max(0, 100 - total * 15)
    passed = total == 0
    return CheckResult(
        rule_id=self.rule_id,
        passed=passed,
        message=f"Vulnerabilities: {critical} critical, {high} high",
        severity=Severity.ERROR if not passed else Severity.INFO,
        score=score,
        details={"high": high, "critical": critical},
        fix_hint="Run: npm audit fix" if total else None,
    )