Skip to content

The annotation language

GAL is a small language with twenty verbs. This page is about how they fit together: which ones declare things, which ones make a claim about code, and what has to be true before a finding appears or goes away. For the syntax of each verb, the GAL reference is generated from guardlink gal and lists all of them.

Nouns are declared once, claims reference them

Section titled “Nouns are declared once, claims reference them”

Every verb is one of two kinds.

Declarations name a thing and give it an #id. @asset, @threat, @control, and @actor live together in .guardlink/definitions.js, and they are the vocabulary the rest of the model draws on.

.guardlink/definitions.js
// @asset App.Users (#users)
// @threat SQL_Injection (#sqli) [critical] cwe:CWE-89
// @control Parameterized_Queries (#prepared-stmts)

Claims say something about a specific place in the code, and they reference those ids rather than repeating their names.

.guardlink/annotations/src/users.js.gal
@source file:src/users.js line:3 symbol:findUserByEmail
@exposes #users to #sqli [critical] -- "email reaches the SQL string"

The split is what makes the model checkable. A claim that references #sqli either resolves to a declared threat or it does not, and guardlink validate fails on the second case with a dangling-reference error. Names alone could not do that: two spellings of “SQL injection” would silently be two threats.

@exposes is the verb that creates work. Everything else either closes that work, escalates it, or records a fact beside it.

flowchart TB
    D["@asset, @threat, @control<br/>declared once, each with an #id"] --> E["@exposes<br/>opens a finding"]
    E --> Q{"what is claimed next?"}
    Q -->|"@mitigates"| M["closed, and a control is named"]
    Q -->|"@accepts"| A["closed, and a person owns the decision"]
    Q -->|"@confirmed"| C["still open, now verified exploitable"]
    Q -->|"nothing"| O["still open, and ci --strict fails"]

    class M,A emphasis

Only two verbs close a finding, and they close it for different reasons. @mitigates says a control handles it and names which one. @accepts says a person decided to carry the risk. Both remove it from the open list, and the model keeps the distinction, so “we fixed it” and “we decided not to” never collapse into the same silence.

The @exposes stays in both cases. Deleting it would delete the reason the control exists, and a later reader would have no way to tell a path that was never risky from one that was made safe.

The remaining verbs do not open or close anything. They attach facts that a reader, a report, or a downstream tool needs.

What you are recording Verbs
How data moves, and where trust changes @flows, @boundary
What an asset holds and who answers for it @handles, @owns, @audit
What you are taking on faith, and what checks a control @assumes, @validates
Who is legitimately allowed to do what @actor, @entitles
Responsibility moved elsewhere @transfers
Grouping, notes, and sensitive code @feature, @comment, @shield

@entitles is worth singling out. It answers “is this caller already allowed to do this by design?”, and it deliberately has no power to suppress anything: it never closes a finding, never gates testing, and does not appear in the SARIF export at all. It only informs the recommendation a downstream tool makes. It also cannot be written by hand or by an agent, because an over-grant would close a real privilege escalation as by-design, so a proposal goes through guardlink entitle and a human accepts it.

GAL describes claims about code, and it stops at the edge of the repository.

It has no verb for a deployment topology, a vendor relationship, or a process, because none of those has a file:line to anchor to. A threat model that needs those still needs prose somewhere, and guardlink does not pretend otherwise.

More importantly, none of these verbs is checked against what the code actually does. @mitigates ... using #prepared-stmts is a statement that a person wrote down. GuardLink parses it, resolves its references, and reports it. It never reads the query to see whether the parameter is really bound.