Behaviour and API for BEAM program analyses.
Each analysis is a module that implements this behaviour, declaring its
name, description, Souffle rules file, required extractors, and output
relations. The system discovers these modules at runtime from the
:panoptes application's module list.
Defining a custom analysis
Create a module that implements @behaviour Argus.Analysis:
defmodule MyApp.Analyses.Unused do
@behaviour Argus.Analysis
@impl true
def name, do: :unused
@impl true
def description, do: "find unused functions"
@impl true
def rules_file, do: "unused.dl"
@impl true
def extractors, do: []
@impl true
def output_relations do
[
%{
name: :unused_function,
fields: [{:func, :symbol, "function ID"}],
doc: "Function that is never called."
}
]
end
endYou can also pass {:custom, "path/to/rules.dl"} to run/3 to run
ad-hoc Datalog rules without defining a module.
Built-in analyses
See modules under Argus.Analyses.* for the full list. Use
builtin_analyses/0 or builtin_analysis_modules/0 to discover them
at runtime.
Summary
Callbacks
Converts one output-relation row into finding attributes.
Functions
Returns the list of built-in analysis names.
Returns all discovered built-in analysis modules.
Derives the stage-0 relations into an existing facts directory.
Extracts facts from the given modules once, for one or more analyses.
Looks up a built-in analysis module by name.
Restricts raw Souffle results to the relations a built-in analysis declares as its outputs.
The relations an analysis actually reads, as Souffle resolves them.
Returns output relations for a named built-in analysis.
Runs an analysis against the given modules.
Runs a single analysis's Datalog rules against an existing facts directory.
The path to the stage-0 rules file.
Types
Callbacks
@callback description() :: String.t()
@callback extractors() :: [module()]
@callback finding(relation :: atom(), row :: [String.t()]) :: Argus.Findings.attrs()
Converts one output-relation row into finding attributes.
Receives the relation name (as declared in output_relations/0) and the
raw row (a list of strings, one per declared field). Implementations
assign a severity, write title/detail prose, and attach the most precise
anchor the row allows — see Argus.Findings for the construction
helpers. Optional: analyses without it fall back to a generic
:info-severity rendering of the relation's declared doc.
Relations that carry witness columns (a call site that evidences the
defect) can produce several rows for one logical finding — one per
witnessing site. Such a relation declares :key: the field names that
identify the finding. Rows agreeing on the key fields are deduplicated
before conversion, and finding/2 receives one deterministic
representative (the lexicographically least row), so finding counts do
not depend on how many sites witness the same defect.
@callback name() :: atom()
@callback output_relations() :: [output_relation()]
@callback rules_file() :: String.t()
Functions
@spec builtin_analyses() :: [atom()]
Returns the list of built-in analysis names.
@spec builtin_analysis_modules() :: [module()]
Returns all discovered built-in analysis modules.
Derives the stage-0 relations into an existing facts directory.
Stage 0 is the shared call graph (call_edge): every client analysis
needs it, and before stratification each one re-derived it inside its
own solve from the layer-1 bytecode relations. Deriving it once here
removes that redundancy, and — more importantly for incremental
consumers — keeps instruction, remote_call and friends out of the
input set of analyses that only reason about supervision structure.
extract_facts/3 calls this for you, so batch callers need not think
about it. Incremental consumers call it directly, memoize the result,
and reuse it across solves: the output is markedly more stable than its
inputs, since it moves only when the call structure changes, not when
a function body does.
Writes call_edge.facts into facts_dir. Idempotent — re-running
overwrites with the same content for the same inputs.
@spec extract_facts(modules :: [atom() | String.t()], [analysis()], keyword()) :: {:ok, Path.t()} | {:error, term()}
Extracts facts from the given modules once, for one or more analyses.
Runs the pipeline with the union of the analyses' default extractors
(plus any extra :extractors from opts), writing .facts files to a
fresh temporary directory. Because the pipeline always materializes
every schema relation (empty files included), the resulting directory
can feed run_rules/3 for each of the analyses without re-extraction.
Returns {:ok, facts_dir} or {:error, reason}.
Looks up a built-in analysis module by name.
Returns {:ok, module} or :error if not found.
Restricts raw Souffle results to the relations a built-in analysis declares as its outputs.
Intermediate clientlib relations (call_reachable, sync_dep, ...) are
dropped. Custom analyses and unknown names pass through unchanged — there
is no declaration to filter against.
The relations an analysis actually reads, as Souffle resolves them.
Derived from the transformed RAM program — the form that actually
executes — rather than the source .dl. That distinction matters: the
parsed AST lists every declared input including ones later pruned as
unused, so reading the source over-approximates, and following
.include by hand under-approximates (Souffle resolves includes
relative to the including file). The RAM's operation="input" entries
are the set Souffle will genuinely open.
Incremental consumers use this to project a per-analysis fact directory, so an analysis only re-solves when a relation it truly reads has moved.
Returns {:ok, [relation_name]} or {:error, reason}.
@spec output_relations(atom()) :: {:ok, [output_relation()]} | :error
Returns output relations for a named built-in analysis.
Returns {:ok, relations} or :error if the analysis is not found.
@spec run(modules :: [atom() | String.t()], analysis(), keyword()) :: {:ok, result()} | {:error, term()}
Runs an analysis against the given modules.
Returns {:ok, results} where results is a map of relation name to
list of rows. Each row is a list of strings.
Options
:concurrency— number of parallel extraction workers (default: schedulers):extractors— list of domain extractor modules to run:souffle_bin— path to souffle binary (default: auto-detect)
Runs a single analysis's Datalog rules against an existing facts directory.
The facts directory must contain .facts files for every relation the
analysis declares as input — extract_facts/3 guarantees this when the
analysis was included in its analyses list.
Returns {:ok, results} or {:error, reason}.
@spec stage0_rules_path() :: Path.t()
The path to the stage-0 rules file.