Argus.Findings (Panoptes v0.13.0)

Copy Markdown View Source

Structured findings from running analyses in-process.

run/2 (exposed as Argus.run_analyses/2) extracts facts once, evaluates each selected analysis's Datalog rules against the shared facts directory, and converts every output-relation row into a finding map with a severity, human-readable prose, and the most precise anchor the row allows:

  • instr — an Argus.InstrId when the row carries an instruction ID.
  • mfa{module, function, arity} when the row carries a function ID (or names a well-known callback such as init/1).
  • module — always set when the row names a module at all.

Degradation

The promise is degrade-, never crash-, and never silently:

  • Souffle missing from PATH → {:error, :souffle_not_found} up front.
  • Fact extraction failing (unknown module, unreadable .beam) → {:error, reason} — nothing could have run.
  • A single analysis erroring (rules bug, Souffle timeout) → a degraded entry naming the analysis and why, while every other analysis still runs and reports.

Atom creation

Anchor parsing converts module and function name strings back to atoms with String.to_atom/1. Those names come from BEAM files the caller asked Argus to disassemble, so the atoms already exist in this node's atom table — parsing does not grow it. Do not feed findings from untrusted .beam files into a long-lived node; run Argus in a sandbox process instead (this is how lowdown consumes uploads).

Summary

Types

Code location attached to a finding, most precise field wins.

What an analysis module's finding/2 callback returns.

Per-analysis degradation note for analyses that did not complete.

A finding: attrs plus the analysis that produced it.

Per-analysis run record for analyses that completed.

A labelled secondary location (sibling, supervisor, callee, ...).

Finding severity, in decreasing order of urgency.

t()

Functions

Anchor for a function ID string ("Mod:func/arity").

Anchor for an instruction ID string ("Mod:func/arity#idx").

Anchor for a known callback on a module string.

Anchor for a module string ("MyApp.Cache" or ":lists").

Anchor for a site ID of either precision, falling back to a module.

Deduplicates a relation's rows down to one per logical finding.

Converts an inspect/1-rendered module string back to the module atom.

Builds finding attributes.

Labels an anchor as a secondary location.

Runs analyses against the given modules and returns structured findings.

Types

anchor()

@type anchor() :: %{
  module: module() | nil,
  mfa: mfa() | nil,
  instr: Argus.InstrId.t() | nil
}

Code location attached to a finding, most precise field wins.

attrs()

@type attrs() :: %{
  severity: severity(),
  title: String.t(),
  detail: String.t(),
  module: module() | nil,
  mfa: mfa() | nil,
  instr: Argus.InstrId.t() | nil,
  at_label: String.t() | nil,
  help: [String.t()],
  related: [related()]
}

What an analysis module's finding/2 callback returns.

degradation()

@type degradation() :: %{analysis: atom(), reason: term(), detail: String.t()}

Per-analysis degradation note for analyses that did not complete.

finding()

@type finding() :: %{
  analysis: atom(),
  severity: severity(),
  title: String.t(),
  detail: String.t(),
  module: module() | nil,
  mfa: mfa() | nil,
  instr: Argus.InstrId.t() | nil,
  at_label: String.t() | nil,
  help: [String.t()],
  related: [related()]
}

A finding: attrs plus the analysis that produced it.

ran_entry()

@type ran_entry() :: %{
  analysis: atom(),
  duration_ms: non_neg_integer(),
  finding_count: non_neg_integer()
}

Per-analysis run record for analyses that completed.

related()

@type related() :: %{
  label: String.t(),
  module: module() | nil,
  mfa: mfa() | nil,
  instr: Argus.InstrId.t() | nil
}

A labelled secondary location (sibling, supervisor, callee, ...).

severity()

@type severity() :: :error | :warning | :info

Finding severity, in decreasing order of urgency.

t()

@type t() :: %Argus.Findings{
  degraded: [degradation()],
  findings: [finding()],
  ran: [ran_entry()]
}

Functions

at_func(func_id)

@spec at_func(String.t()) :: anchor()

Anchor for a function ID string ("Mod:func/arity").

at_instr(id)

@spec at_instr(String.t()) :: anchor()

Anchor for an instruction ID string ("Mod:func/arity#idx").

Unparseable input (a "dynamic" placeholder, free-form text) yields an empty anchor rather than an error — anchors are best-effort by design.

at_mfa(module_string, func, arity)

@spec at_mfa(String.t(), atom(), arity()) :: anchor()

Anchor for a known callback on a module string.

Several relations report a module known to implement a specific callback (init/1, handle_cast/2, ...) without carrying a function ID — this reconstructs the precise anchor.

at_module(module_string)

@spec at_module(String.t()) :: anchor()

Anchor for a module string ("MyApp.Cache" or ":lists").

at_site(id, module_string)

@spec at_site(String.t(), String.t()) :: anchor()

Anchor for a site ID of either precision, falling back to a module.

Witness columns hold an instruction ID where the extractor had one and a function ID otherwise; extractors mark sites they cannot resolve with a "dynamic" placeholder. This tries the most precise parse first — instruction, then function, then the module fallback — so a finding never loses its module anchor to an unresolvable site.

dedupe_rows(arg1, rows)

@spec dedupe_rows(Argus.Analysis.output_relation(), [[String.t()]]) :: [[String.t()]]

Deduplicates a relation's rows down to one per logical finding.

Relations with witness columns yield one row per witnessing site; rows that agree on the relation's declared :key fields describe the same finding. Keeps the lexicographically least row of each group — a deterministic representative, so finding counts and anchors never depend on Souffle's row order or on how many sites witness the same defect. Relations without a :key pass through unchanged.

Public because in-process embedders that build findings themselves (the planchette pattern) must apply the same identity rule or their counts drift from run/2's.

module_atom(alias_string)

@spec module_atom(String.t()) :: module() | nil

Converts an inspect/1-rendered module string back to the module atom.

Returns nil for the "dynamic" placeholder and anything else that isn't a module rendering.

new(severity, title, detail, opts \\ [])

@spec new(severity(), String.t(), String.t(), keyword()) :: attrs()

Builds finding attributes.

opts:

  • :at — an anchor from at_instr/1, at_func/1, at_module/1, or at_mfa/3 (default: no anchor).
  • :at_label — what the anchor line IS, for renderers that excerpt the source ("supervision tree defined here"), so the annotation does not just repeat the title (default: nil).
  • :help — resolution guidance, one string per suggestion, rendered by consumers as help trailers. Say what to change and toward what, in the row's own terms (default: []).
  • :related — list of related/2 entries (default: []).

related(label, anchor)

@spec related(String.t(), anchor()) :: related()

Labels an anchor as a secondary location.

run(modules, opts \\ [])

@spec run(
  modules :: [atom() | String.t()],
  keyword()
) :: {:ok, t()} | {:error, term()}

Runs analyses against the given modules and returns structured findings.

modules is a list of module atoms or paths to .beam files, exactly as Argus.analyze/3 accepts.

Options

  • :analyses:all (default) or a list of built-in analysis names. :all means every built-in analysis except :coverage, which measures the extractor pipeline rather than the analyzed code.
  • :facts_dir — a directory Argus.Analysis.extract_facts/3 already wrote for these modules, to evaluate without extracting again. The caller owns it; without this option the run extracts into a temporary directory and removes it afterwards.
  • :concurrency — parallel Souffle solves (default: the scheduler count, capped at 4; each solve holds its own copy of the call graph's closure). Extraction always runs at scheduler width.
  • All other Argus.Analysis.run/3 options (:extractors, :souffle_bin, :souffle_timeout, ...) pass through.

Returns {:ok, %Argus.Findings{}} or {:error, reason} — see the moduledoc for the degradation contract.