Skip to content

Repository files navigation

codeql-ascent

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.

Compatibility boundary

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 problem or @kind path-problem results; and
  • the dispatcher adds the carrier and its --external CSV 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.

Quick start: project folder as a query

Rust 1.88 or newer and an official CodeQL bundle are required.

1. Build the dispatcher

From this repository:

cargo build --release -p codeql-ascent-cli --bins
export CODEQL_ASCENT_REAL_CODEQL=/absolute/path/to/official/codeql

There are two front ends:

  • target/release/codeql-ascent is the explicit, low-risk command name;
  • target/release/codeql is 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 version

Ordinary commands such as codeql version, database create, query run, and unmarked query folders pass directly to the official executable.

2. Initialize an Ascent analysis

The compatibility frontend gives native providers their own namespace:

codeql ascent init \
  --language=rust \
  --codeql-ascent-path="$PWD" \
  ascent-project

The 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.

3. Create a normal database

No retained TRAP or --no-cleanup option is needed:

codeql database create \
  --language=rust \
  --build-mode=none \
  codeql-db

The 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.

4. Edit the generated analysis

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.

5. Run it

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.

Result profiles

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.

Library-only integration

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.bqrs

Finalized database and TRAP importers

The default project workflow reads the finalized extensional store:

  • legacy headerless fixed-width, big-endian .rel tuples;
  • the version 241204, subformat 0 compressed-page representation selected by an adjacent .rel.meta file, 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/T and 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.

Tests

Self-contained validation:

cargo fmt --all --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-targets

The ignored conformance tests use an external official CodeQL installation:

CODEQL_BIN=/absolute/path/to/codeql \
  cargo test --workspace -- --ignored --nocapture

They 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

Current boundaries

  • Parses relation declarations and database-type unions from .dbscheme.
  • Loads only CodeQL relations referenced by the Ascent program from finalized .rel data or retained TRAP.
  • Generates CodeQL-compatible @kind problem results 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.

License

MIT. See LICENSE.

About

Run Ascent over CodeQL extractor facts and return results through the official CodeQL CLI

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages