maelys-git

Level 3 — Rich Git context

Add files, branch classes, repository state and trustworthy identity.

Continue from Level 2 — Structured deny codes. Keep the declarations and helpers from the earlier levels; this page extends the same wrapper in C and Python.

Level 3 enriches the EDB with facts extracted from Git state: staged files, branch families, working-tree status, sensitive-file classification, and approval markers. The wrapper performs string matching and Git inspection in C; the Datalog policy only reasons over already-classified facts. This keeps the policy deterministic, bounded, and auditable.

Classify context in the wrapper

The engine has bounded string filters, including starts_with. Here we deliberately classify paths and branch families in the wrapper so the policy reasons about application concepts such as sensitive_file_staged(File) and branch_class("release"). C and Python must produce the same normalized facts.

Extended domain

CODEExtend the Git vocabulary
static const maelys_datalog_predicate_t git_preds_l3[] = {
  { "operation",            1, MAELYS_DATALOG_PREDICATE_EDB },
  { "target_ref",           1, MAELYS_DATALOG_PREDICATE_EDB },
  { "source_ref",           1, MAELYS_DATALOG_PREDICATE_EDB },
  { "user",                 1, MAELYS_DATALOG_PREDICATE_EDB },
  { "flag",                 1, MAELYS_DATALOG_PREDICATE_EDB },
  { "commit_msg",           1, MAELYS_DATALOG_PREDICATE_EDB },
  { "valid_commit_msg",     1, MAELYS_DATALOG_PREDICATE_EDB },

  /* Level 3: richer context — C extracts, policy decides */
  { "changed_file",         1, MAELYS_DATALOG_PREDICATE_EDB },
  { "sensitive_file_staged",1, MAELYS_DATALOG_PREDICATE_EDB },
  { "approval_present",     1, MAELYS_DATALOG_PREDICATE_EDB },
  { "working_tree_dirty",   1, MAELYS_DATALOG_PREDICATE_EDB },
  { "branch_class",         1, MAELYS_DATALOG_PREDICATE_EDB },

  /* POLICY_FACT */
  { "known_operation", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
  { "protected_branch",     1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
  { "blocked_user",         1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
  { "release_manager",      1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
  /* sensitive_path is not needed: C classifies files into sensitive_file_staged */

  /* IDB */
  { "deny",    2, MAELYS_DATALOG_PREDICATE_IDB | MAELYS_DATALOG_PREDICATE_QUERY },
  { "deny_any",1, MAELYS_DATALOG_PREDICATE_IDB },
  { "allow",   1, MAELYS_DATALOG_PREDICATE_IDB | MAELYS_DATALOG_PREDICATE_QUERY },
};

Extended policy

DATALOG
/* Append these rules to the Level 1 policy. The wrapper classifies paths. */

/* ── Force push ─────────────────────────────────────────────── */
deny("push", "GIT_FORCE_PUSH") :- operation("push"), flag("force").

/* ── Protected branch (non-managers only) ───────────────────── */
deny("push", "GIT_PROTECTED_BRANCH") :-
    operation("push"), target_ref(B), protected_branch(B),
    user(U), not(release_manager(U)).

/* ── Release branch family (C injects branch_class("release") for release branches) ── */
/* branch_class("release") covers release/v1, release/v2, etc.
   protected_branch("release") above covers only the branch named exactly "release".
   These two rules are complementary, not redundant. */
deny("push", "GIT_RELEASE_BRANCH") :-
    operation("push"),
    branch_class("release"),
    user(U),
    not(release_manager(U)).

/* ── Blocked user ───────────────────────────────────────────── */
deny("commit", "GIT_BLOCKED_USER") :-
    operation("commit"), user(U), blocked_user(U).

/* ── Commit message prefix ──────────────────────────────────── */
deny("commit", "GIT_BAD_COMMIT_PREFIX") :-
    operation("commit"), commit_msg(M), not(valid_commit_msg(M)).

/* ── Sensitive file requires approval ───────────────────────── */
deny("commit", "GIT_SENSITIVE_FILE") :-
    operation("commit"),
    sensitive_file_staged(F),
    not(approval_present(F)).

/* ── Dirty working tree cannot be pushed ───────────────────── */
deny("push", "GIT_DIRTY_TREE") :-
    operation("push"),
    working_tree_dirty("true").

/* ── deny_any / allow ───────────────────────────────────────── */
deny_any(Op) :- deny(Op, Code).
allow(Op)    :- operation(Op), known_operation(Op), not(deny_any(Op)).

Extended EDB population

CODEAdd repository evidence before solving
typedef int (*path_check_t)(const char *path); /* 1 yes, 0 no, -1 error */

static maelys_datalog_status_t populate_extended_edb(
    maelys_datalog_input_edb_t *edb, const git_context_t *ctx,
    const char *const *staged, size_t n_staged,
    path_check_t is_sensitive_path, path_check_t has_approval,
    int working_tree_dirty, maelys_datalog_diagnostic_t *diagnostic)
{
    if (!is_sensitive_path || !has_approval || working_tree_dirty < 0)
        return MAELYS_DATALOG_STATUS_INVALID_ARGUMENT;
    maelys_datalog_status_t rc;
    if (ctx->source_ref) {
        rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "source_ref", ctx->source_ref);
        if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
    }
    if (ctx->target_ref && strncmp(ctx->target_ref, "release/", 8) == 0) {
        rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "branch_class", "release");
        if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
    }
    for (size_t i = 0; i < n_staged; ++i) {
        int sensitive = is_sensitive_path(staged[i]);
        int approved = has_approval(staged[i]);
        if (sensitive < 0 || approved < 0) return MAELYS_DATALOG_STATUS_IO;
        rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "changed_file", staged[i]);
        if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
        if (sensitive) {
            rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "sensitive_file_staged", staged[i]);
            if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
        }
        if (approved) {
            rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "approval_present", staged[i]);
            if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
        }
    }
    if (working_tree_dirty) {
        rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "working_tree_dirty", "true");
        if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
    }
    return MAELYS_DATALOG_STATUS_OK;
}

Trustworthy identity: not from the environment

The parser callback must supply authenticated identity. Environment variables are attacker-controlled: any caller can run USER=alice maelys-git push … and impersonate a release manager. A real gate must derive identity from the operating system, not from the environment it was handed.

CODERead the operating-system identity
#include <unistd.h>
#include <pwd.h>

/* Real identity: the uid the process runs as, resolved to a name.
   Not spoofable through the environment. */
static const char *real_user(void) {
    struct passwd *pw = getpwuid(getuid());
    return pw ? pw->pw_name : NULL;
}

What you have learned

Keep this model

Give policy the repository context it needs
  • Declare the additional evidence

    Staged files, branch classes, tree state and approvals become facts in the extended domain. Rules can then make decisions using that vocabulary.

  • Extract and classify outside the rules

    The wrapper observes repository state and turns it into canonical facts. Policy uses those facts rather than parsing command-line or path strings.

  • Identity needs a trustworthy source

    The example obtains the operating-system identity rather than trusting a user-editable environment variable. The wrapper remains responsible for collecting complete, reliable evidence.

Continue