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,
)
|