Skip to content

base

_base

Shared base for Node-ecosystem rules.

Every node rule repeats the same preamble: skip cleanly when the project is not a node project (no package.json), fail loud when the required CLI tool is not installed locally (never a false green), and treat an env-failure exit (timeout / missing config) as a hard fail rather than a clean zero-findings run. NodeToolRule factors that out so each rule only implements score_output.

NodeToolRule

Bases: ProjectRule

Base for a node rule that scores the output of one local CLI tool.

Subclasses set :attr:binary / :attr:install_hint and implement :meth:score_output (and optionally :attr:args / :meth:parse_json). The base handles the no-package.json skip, tool-availability fail-loud, the subprocess run, env-failure detection, and JSON parsing.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/_base.py
Python
class NodeToolRule(ProjectRule):
    """Base for a node rule that scores the output of one local CLI tool.

    Subclasses set :attr:`binary` / :attr:`install_hint` and implement
    :meth:`score_output` (and optionally :attr:`args` / :meth:`parse_json`).
    The base handles the no-package.json skip, tool-availability fail-loud, the
    subprocess run, env-failure detection, and JSON parsing.
    """

    binary: str = ""
    """Executable this rule drives (e.g. ``eslint``, ``npm``, ``gitleaks``)."""

    install_hint: str = ""
    """Human hint shown when the binary is not installed."""

    on_path: bool = False
    """If True, :attr:`binary` is a global PATH command (``npm``, ``gitleaks``)
    rather than a project-local ``node_modules/.bin`` binary."""

    @property
    def args(self) -> list[str]:
        """CLI arguments passed to :attr:`binary` (override per rule)."""
        return []

    @property
    def findings_returncodes(self) -> frozenset[int]:
        """Exit codes that mean "ran fine, reported findings" (not env failure).

        The shared env-failure set treats rc in {2, 124} as a tool that did not
        complete — correct for eslint/ruff/mypy, but ``tsc`` exits **2** when it
        simply *found* type errors. A rule whose tool overloads such a code adds
        it here so the base scores the output instead of failing loud. Default
        is empty (defer entirely to :func:`interpret_process`).
        """
        return frozenset()

    @abstractmethod
    def score_output(self, parsed: object, project_path: Path) -> CheckResult:
        """Turn the parsed tool output into a scored :class:`CheckResult`.

        Args:
            parsed: The parsed stdout (JSON by default, see :meth:`parse_json`).
            project_path: Project root (for relative paths in findings).

        Returns:
            The scored result for this rule.
        """

    def parse(self, result: subprocess.CompletedProcess[str]) -> object:
        """Parse the finished subprocess into the value :meth:`score_output` wants.

        Default: JSON-decode stdout (tolerating empty/invalid output → ``[]``).
        Text tools (``tsc``, ``prettier``) override this to return raw stdout —
        or the combined stdout+stderr when the tool reports on stderr.
        """
        stdout = result.stdout
        if not stdout.strip():
            return []
        try:
            return json.loads(stdout)
        except json.JSONDecodeError:
            return []

    def check(self, project_path: Path) -> CheckResult:
        """Run the tool and delegate scoring, with the shared safety preamble."""
        if not (project_path / "package.json").is_file():
            return CheckResult(
                rule_id=self.rule_id,
                passed=True,
                message=f"No package.json — {self.binary} skipped",
                severity=Severity.INFO,
                score=100,
            )
        available = (
            path_tool_available(self.binary)
            if self.on_path
            else node_tool_available(project_path, self.binary)
        )
        if not available:
            where = "PATH" if self.on_path else f"node_modules/.bin/{self.binary}"
            return CheckResult(
                rule_id=self.rule_id,
                passed=False,
                message=f"{self.binary} not available (not on {where})",
                severity=Severity.ERROR,
                fix_hint=self.install_hint,
            )

        result = run_node_tool(
            self.binary, self.args, project_path, on_path=self.on_path
        )
        is_findings_code = result.returncode in self.findings_returncodes
        if (
            not is_findings_code
            and interpret_process(result) is ProcessVerdict.ENV_FAILURE
        ):
            return self.env_failure_result(result.returncode)

        return self.score_output(self.parse(result), project_path)

    def env_failure_result(self, returncode: int) -> CheckResult:
        """Fail-loud result when the tool did not complete (timeout/config)."""
        diagnostic = (
            f"audit environment unreliable — {self.binary} did not complete "
            f"(exit code {returncode}: missing config/deps or timeout). "
            f"Run `npm install` and ensure {self.binary} is configured."
        )
        return CheckResult(
            rule_id=self.rule_id,
            passed=False,
            message=f"{self.rule_id} BLOCKED: {diagnostic}",
            severity=Severity.ERROR,
            score=0,
            details={"env_incomplete": True},
            fix_hint=diagnostic,
        )
args property

CLI arguments passed to :attr:binary (override per rule).

binary = '' class-attribute instance-attribute

Executable this rule drives (e.g. eslint, npm, gitleaks).

findings_returncodes property

Exit codes that mean "ran fine, reported findings" (not env failure).

The shared env-failure set treats rc in {2, 124} as a tool that did not complete — correct for eslint/ruff/mypy, but tsc exits 2 when it simply found type errors. A rule whose tool overloads such a code adds it here so the base scores the output instead of failing loud. Default is empty (defer entirely to :func:interpret_process).

install_hint = '' class-attribute instance-attribute

Human hint shown when the binary is not installed.

on_path = False class-attribute instance-attribute

If True, :attr:binary is a global PATH command (npm, gitleaks) rather than a project-local node_modules/.bin binary.

check(project_path)

Run the tool and delegate scoring, with the shared safety preamble.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/_base.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Run the tool and delegate scoring, with the shared safety preamble."""
    if not (project_path / "package.json").is_file():
        return CheckResult(
            rule_id=self.rule_id,
            passed=True,
            message=f"No package.json — {self.binary} skipped",
            severity=Severity.INFO,
            score=100,
        )
    available = (
        path_tool_available(self.binary)
        if self.on_path
        else node_tool_available(project_path, self.binary)
    )
    if not available:
        where = "PATH" if self.on_path else f"node_modules/.bin/{self.binary}"
        return CheckResult(
            rule_id=self.rule_id,
            passed=False,
            message=f"{self.binary} not available (not on {where})",
            severity=Severity.ERROR,
            fix_hint=self.install_hint,
        )

    result = run_node_tool(
        self.binary, self.args, project_path, on_path=self.on_path
    )
    is_findings_code = result.returncode in self.findings_returncodes
    if (
        not is_findings_code
        and interpret_process(result) is ProcessVerdict.ENV_FAILURE
    ):
        return self.env_failure_result(result.returncode)

    return self.score_output(self.parse(result), project_path)
env_failure_result(returncode)

Fail-loud result when the tool did not complete (timeout/config).

Source code in packages/axm-audit/src/axm_audit/core/rules/node/_base.py
Python
def env_failure_result(self, returncode: int) -> CheckResult:
    """Fail-loud result when the tool did not complete (timeout/config)."""
    diagnostic = (
        f"audit environment unreliable — {self.binary} did not complete "
        f"(exit code {returncode}: missing config/deps or timeout). "
        f"Run `npm install` and ensure {self.binary} is configured."
    )
    return CheckResult(
        rule_id=self.rule_id,
        passed=False,
        message=f"{self.rule_id} BLOCKED: {diagnostic}",
        severity=Severity.ERROR,
        score=0,
        details={"env_incomplete": True},
        fix_hint=diagnostic,
    )
parse(result)

Parse the finished subprocess into the value :meth:score_output wants.

Default: JSON-decode stdout (tolerating empty/invalid output → []). Text tools (tsc, prettier) override this to return raw stdout — or the combined stdout+stderr when the tool reports on stderr.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/_base.py
Python
def parse(self, result: subprocess.CompletedProcess[str]) -> object:
    """Parse the finished subprocess into the value :meth:`score_output` wants.

    Default: JSON-decode stdout (tolerating empty/invalid output → ``[]``).
    Text tools (``tsc``, ``prettier``) override this to return raw stdout —
    or the combined stdout+stderr when the tool reports on stderr.
    """
    stdout = result.stdout
    if not stdout.strip():
        return []
    try:
        return json.loads(stdout)
    except json.JSONDecodeError:
        return []
score_output(parsed, project_path) abstractmethod

Turn the parsed tool output into a scored :class:CheckResult.

Parameters:

Name Type Description Default
parsed object

The parsed stdout (JSON by default, see :meth:parse_json).

required
project_path Path

Project root (for relative paths in findings).

required

Returns:

Type Description
CheckResult

The scored result for this rule.

Source code in packages/axm-audit/src/axm_audit/core/rules/node/_base.py
Python
@abstractmethod
def score_output(self, parsed: object, project_path: Path) -> CheckResult:
    """Turn the parsed tool output into a scored :class:`CheckResult`.

    Args:
        parsed: The parsed stdout (JSON by default, see :meth:`parse_json`).
        project_path: Project root (for relative paths in findings).

    Returns:
        The scored result for this rule.
    """