codeql-ascent runs Ascent rules over a
normal finalized CodeQL database, then lets the official CodeQL CLI produce the
usual BQRS or SARIF result. It is experimental, unofficial, and not affiliated
with GitHub.
normal CodeQL database
.dbscheme + selected .rel relations + string pool
|
v
typed Rust facts -> Ascent fixed point
|
v
CSV + generated carrier.ql
|
v
official CodeQL evaluator
|
v
BQRS / SARIF
The optional dispatcher makes an explicitly marked Cargo/Ascent project look like one CodeQL query-folder selector:
codeql database analyze \
--format=sarifv2.1.0 \
--output=results.sarif \
-- \
codeql-db \
ascent-project/The stock CLI does not have a provider hook for non-QL files. The executable
named codeql above is therefore a thin dispatcher placed before the official
binary on PATH. It intercepts codeql ascent init and marked project
selectors for database analyze in the standard OPTIONS -- DATABASE SELECTORS... form; every other invocation is forwarded unchanged. It does not
reimplement the QL compiler, evaluator, BQRS, or SARIF.
The provider-v1 compatibility seam is one or more complete native analyses in
a single database analyze invocation. Each exported query uses one of three
versioned carrier profiles: located-problem-v1, problem-v2, or
path-problem-v1.
- the command shape, database, query identity and metadata, BQRS storage, SARIF output, and downstream consumers remain CodeQL-native;
- Ascent replaces the recursive reasoning that derives the result relation;
- generated QL carriers convert primitive finding, related-location, or path
relations into official
@kind problemor@kind path-problemresults; and - the dispatcher adds the carrier and its
--externalCSV binding, then forces reevaluation so stale external input is never reused.
This does not execute arbitrary .ql as Ascent. It is not a general
replacement for QL compilation, packs or suites, query run with a native
project selector, the split database run-queries / interpret-results
workflow, intermediate QL predicates, or arbitrary result profiles beyond the
three listed above. It also does not automatically reproduce CodeQL library
predicates such as data flow. Those need explicit equivalent Ascent rules and,
for a cross-engine predicate seam, a stable entity-key contract.
Rust 1.88 or newer and an official CodeQL bundle are required.
From this repository:
cargo build --release -p codeql-ascent-cli --bins
export CODEQL_ASCENT_REAL_CODEQL=/absolute/path/to/official/codeqlThere are two front ends:
target/release/codeql-ascentis the explicit, low-risk command name;target/release/codeqlis the same dispatcher under the compatibility name.
For the exact command spelling, put target/release before the official bundle
on PATH. CODEQL_ASCENT_REAL_CODEQL removes any ambiguity about which binary
is delegated to:
export PATH="$PWD/target/release:$PATH"
codeql versionOrdinary commands such as codeql version, database create, query run, and
unmarked query folders pass directly to the official executable.
The compatibility frontend gives native providers their own namespace:
codeql ascent init \
--language=rust \
--codeql-ascent-path="$PWD" \
ascent-projectThe explicit frontend is equivalent:
target/release/codeql-ascent init \
--language=rust \
--codeql-ascent-path="$PWD" \
ascent-project--language asks the official CLI to resolve that language's extractor and
copies its one top-level .dbscheme file. For an offline or deliberately
pinned schema, use --schema=/path/to/language.dbscheme instead.
--codeql-ascent-path is currently required: the initializer records an exact
local runtime path instead of silently generating a floating dependency on an
unreleased Git branch.
Initialization creates a new directory and refuses to overwrite any existing
path. It generates a buildable Cargo/Ascent provider, the explicit project
marker, a copied schema, and an intentionally empty starter result. It does not
create qlpack.yml: this is native trusted Rust code, not a QL pack. Bare
codeql init is left untouched and forwarded; the official
codeql pack init
remains the QL-pack initializer.
No retained TRAP or --no-cleanup option is needed:
codeql database create \
--language=rust \
--build-mode=none \
codeql-dbThe initialized project has already copied the matching extractor schema:
ascent-project/schema/codeql.dbscheme
At runtime, only referenced relation signatures are compared with the
database's own .dbscheme. A schema mismatch fails before any .rel row is
interpreted.
The project root contains:
ascent-project/
Cargo.toml
README.md
codeql-ascent.toml
schema/codeql.dbscheme
src/main.rs
codeql-ascent.toml is the explicit opt-in marker:
version = 1
[provider]
command = "cargo"
args = ["run", "--quiet", "--release"]Until the crates are published, initialized projects use the explicit source
checkout passed to --codeql-ascent-path:
[dependencies]
ascent = "0.8"
codeql-ascent = { path = "/absolute/path/to/codeql-ascent" }Relations written as codeql::name(...) are inferred from the schema,
declared, converted, and bulk-loaded by the macro. There is no relation
manifest and no call that supplies each relation manually:
use codeql_ascent::{EntityId, codeql_ascent};
codeql_ascent! {
schema = "schema/codeql.dbscheme";
pub struct Analysis;
relation call_edge(EntityId, EntityId);
call_edge(call, function) <--
codeql::call_expr_functions(call, function);
#[codeql(
export = "ascentFinding",
id = "rust/ascent-finding",
name = "Ascent finding",
description = "A finding derived by the Ascent analysis.",
severity = "warning",
precision = "high",
tags = "correctness",
)]
relation finding(
i64, &'static str, i64, i64, i64, i64, &'static str
);
// Ordinary Ascent rules derive `finding` from the imported EDB.
}Schema case encodings can be decoded into Rust ADTs at this same boundary.
For example, if py_stmts is physically stored as
(id, kind, parent, index), this declaration:
codeql_ascent! {
schema = "schema/codeql.dbscheme";
adts = [py_stmts as Stmt];
pub struct Analysis;
relation if_statement(EntityId);
if_statement(statement) <--
codeql::py_stmts(?Stmt::If(statement), _, _);
}makes the logical Ascent relation (Stmt, EntityId, i64). The generated
loader consumes the numeric kind column and constructs Stmt::If(id),
Stmt::While(id), and the other variants declared by the schema. Numeric
case tags remain an extractor storage detail and do not appear in rules.
The same declaration also handles entity relations whose idx column selects
a named field on a union-typed parent. For example,
py_stmt_lists as StmtList uses the schema's <Field> metadata plus the
parent entity type to construct variants such as StmtList::IfBody(id) and
StmtList::IfOrelse(id). The logical relation is
(StmtList, EntityId); the physical parent-field index remains confined to
the loader.
Scalar field relations use positions when the value itself has no entity
identity. For example:
codeql_ascent! {
schema = "schema/codeql.dbscheme";
positions = [py_strs as StrPosition];
pub struct Analysis;
relation import_name(EntityId, &'static str);
import_name(expression, name) <--
codeql::py_strs(
name,
StrPosition::ImportExprName(*expression),
);
}The physical py_strs(value, parent, idx) row is decoded while loading.
Logical Ascent sees py_strs(value, StrPosition); named AST fields and
string-list ordinals are represented by different enum variants.
The macro generates a provider descriptor containing the program's registry and load/run/export adapter. The entry point supplies only the independent programs owned by the project:
use std::error::Error;
use codeql_ascent::{ProviderContext, run_provider_programs};
fn main() -> Result<(), Box<dyn Error>> {
let context = ProviderContext::from_env()?;
run_provider_programs(
&context,
&[Analysis::CODEQL_PROVIDER_PROGRAM],
)?;
Ok(())
}For ports of existing CodeQL queries, keep the semantic compilation unit
explicit: one top-level .ql root should produce one Ascent program. Its
transitively imported .qll semantics may be supplied through reusable
sources fragments, and one program may contain many relation SCCs. Separate
.ql roots should not be combined merely because they share a provider
project. run_provider_programs validates the union registry, runs only the
selected programs, prepares each program in a distinct artifact directory,
and writes the combined provider plan.
When Analysis::REQUIRED_RELATIONS is nonempty, from_codeql_database
discovers the compatible finalized db-* dataset and loads exactly those
relations. An export-only program with no CodeQL EDB references returns its
default input state without dataset discovery. prepare_codeql_exports
discovers all annotated result relations. Adding another relation does not add
another manual input or CLI argument in project code.
The executable fixture at
tests/fixtures/hybrid-project is a complete
project using this contract.
Using the exact-name shim:
codeql database analyze \
--format=sarifv2.1.0 \
--output=results.sarif \
-- \
codeql-db \
ascent-project/Or use the explicit front end with the same arguments:
codeql-ascent database analyze \
--format=sarifv2.1.0 \
--output=results.sarif \
-- \
codeql-db \
ascent-project/The dispatcher runs the provider in the project root with canonical database
and invocation-owned output paths. It validates the returned plan, rejects
duplicate external predicates, CodeQL query IDs, and carrier paths across
native projects, injects one --external=<predicate>=<csv> per result
relation, stages every standalone carrier under a bounded ordinal filename,
replaces the project selector with the staged path:<carrier.ql> selectors,
and invokes official CodeQL. Ordinal staging prevents same-named,
case-folding, or unusually long provider paths from colliding in CodeQL's BQRS
output. Multiple marked projects and ordinary QL selectors can coexist in the
same command.
See the provider protocol for the exact manifest, environment, JSON plan, path, failure, and caching contract.
located-problem-v1 uses one finding relation:
(result_id, absolute_path, start_line, start_column,
end_line, end_column, message)
problem-v2 adds a related-location relation:
finding =
(finding_id, absolute_path, start_line, start_column,
end_line, end_column, message)
related =
(finding_id, slot, related_id, absolute_path, start_line,
start_column, end_line, end_column, label)
path-problem-v1 uses finding, path-node, and ordered path-edge relations:
finding = (finding_id, source_node_id, sink_node_id, message, source_label)
node =
(node_id, absolute_path, start_line, start_column,
end_line, end_column, label)
edge = (finding_id, ordinal, from_node_id, to_node_id)
IDs are unique synthetic carrier identities, not imported EntityId values.
Numbers must fit CodeQL's signed 32-bit int; positions are positive and
ordered; source paths are absolute and lexically normalized. The adapters
validate profile-specific ownership and ordering constraints. CSV quoting,
deterministic row ordering, external-predicate declarations, location or path
decoration, and QLDoc metadata are generated automatically.
Imported CodeQL string columns are &'static str. An import-scoped interner
leaks each distinct decoded string-pool, date, or TRAP value once, so duplicate
rows and Ascent indices copy a pointer instead of cloning or leaking the text
again. This follows Ascent's own file-backed examples and fits the normal
provider lifecycle: one child process handles one analysis, then the operating
system reclaims its address space.
For text genuinely constructed in a rule's surrounding Rust code, call
codeql_ascent::leak(value) once before inserting it. Long-running embedding
processes should not repeatedly import databases or call leak; a reclaiming
interner is a separate ownership mode that this provider-oriented API does not
yet expose. Exported path and message types are otherwise generic over
AsRef<str>. In Ascent rules, use bare variables in the head; static string
references are copied without allocation.
The dispatcher always reevaluates generated carriers. This matters because
CodeQL's result cache does not infer that an external CSV changed. Use an
explicit output format such as sarifv2.1.0 rather than the moving
sarif-latest alias.
The dispatcher is optional. An embedding application can perform the same handoff explicitly:
use std::error::Error;
use std::process::Command;
fn run() -> Result<(), Box<dyn Error>> {
let mut analysis = Analysis::from_codeql_database("codeql-db")?;
analysis.run();
let prepared =
analysis.prepare_codeql_exports("target/codeql-ascent")?;
let status = Command::new("codeql")
.args([
"database", "analyze", "--rerun",
"--format=sarifv2.1.0", "--output=results.sarif",
])
.args(prepared.external_options())
.arg("--")
.arg("codeql-db")
.args(prepared.analyze_query_specifiers())
.status()?;
if !status.success() {
return Err(std::io::Error::other(
format!("CodeQL exited with {status}")
).into());
}
Ok(())
}For raw BQRS while developing a carrier:
codeql query run \
--database=codeql-db \
--external=ascentFinding=target/codeql-ascent/codeql-ascent-export-0000.csv \
--output=result.bqrs \
-- \
target/codeql-ascent/codeql-ascent-export-0000.ql
codeql bqrs decode --format=json --entities=all result.bqrsThe default project workflow reads the finalized extensional store:
- legacy headerless fixed-width, big-endian
.reltuples; - the version
241204, subformat0compressed-page representation selected by an adjacent.rel.metafile, as emitted by CodeQL CLI 2.26.1; - 32-bit entities, integers, booleans, and string IDs;
- 64-bit floats and packed dates;
- sequential and non-sequential disk string pools; and
- legacy
Q/Tand compressed nullary relations.
This relation-store encoding is an internal CodeQL format, not a promised public interchange API. The importer therefore checks the exact requested relation signatures, validates row widths or compressed metadata, sentinel headers, page boundaries, tuple counts, endpoints, CRC32 checksums, encodings, and known string-pool versions, and fails rather than guessing on unsupported layouts. The official BQRS/SARIF and marked-project conformance tests have been run against CodeQL CLI 2.19.3 and the latest-tested 2.26.1 release. A fresh 2.26.1 Rust schema parsed successfully, and a fresh finalized database smoke test decoded all relations in its live Python schema before an official carrier round-trip. The self-contained suite separately covers legacy, compressed, mixed-format, checksum, metadata-version, and string-pool failure cases.
The original TRAP path remains available when an extractor boundary is more appropriate:
let mut analysis =
Analysis::from_trap_dir("codeql-db/trap/rust")?;It reads .trap, .trap.gz, and .trap.zst. Retaining those intermediate
files normally requires database create --no-cleanup; some extractors also
need a supported compression override.
Self-contained validation:
cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-targetsThe ignored conformance tests use an external official CodeQL installation:
CODEQL_BIN=/absolute/path/to/codeql \
cargo test --workspace -- --ignored --nocaptureThey cover generated QL through BQRS/SARIF and the marked-project dispatcher workflow. The schema parser can also be checked against a real Rust extractor schema:
CODEQL_RUST_DBSCHEME=/path/to/rust.dbscheme \
cargo test --test upstream_codeql -- --ignored --nocapture- Parses relation declarations and database-type unions from
.dbscheme. - Loads only CodeQL relations referenced by the Ascent program from finalized
.reldata or retained TRAP. - Generates CodeQL-compatible
@kind problemresults with stable metadata. - Does not compile QL, encode BQRS itself, or reproduce CodeQL library predicates automatically.
- Keeps recursive strongly connected components in one engine; cross-engine incremental fixed points require a separate scheduler.
See DESIGN.md for the semantic model.
MIT. See LICENSE.