Skip to main content

g6-alert — Unified Guardsix Alert-Rule SDK & CLI

One CLI and one AlertClient facade for managing Guardsix alert (detection) rules across Standalone instances and Fleet consoles, selected by a typed profile.

Python Version License


Table of Contents


Overview

Guardsix is a SIEM (Security Information and Event Management) platform used by security operations teams to detect threats and investigate incidents. Alert rules (also called detection rules) define the search queries, conditions, risk levels, and MITRE ATT&CK mappings that trigger Guardsix incidents.

Guardsix is deployed in two topologies:

Mode Target Auth Topology
Standalone Single Guardsix instance Local self-signed JWT — no network token endpoint One implicit target
Fleet Fleet console → many pools of nodes Remote Bearer token with /refreshToken refresh Multi-target fan-out; async "order" API

This package drives both from one g6-alert binary and one AlertClient facade. A profile typed as standalone or fleet selects the transport and auth; the same verbs (list, create, activate, …) work against either backend, though the two modes differ in several important ways — see Standalone vs. Fleet: what differs below.

The shared domain layer (path-safe YAML loading, rule validation, pack manifests, exception hierarchy, formatting) lives in the companion package g6-alert-core, which has no HTTP, auth, or CLI dependencies.

The detection rules and packs this CLI installs are published separately at https://gitlab.com/guardsix-oss/detection-rules; check that repo out anywhere on disk and point install-pack / pack-diff at it.


Features

  • One CLI, two backends. g6-alert drives both Standalone and Fleet with the same verbs. Fleet-only commands are hidden from --help under a standalone profile.
  • Unified AlertClient facade for backend-agnostic Python usage — swap profiles without changing application code.
  • Per-target resilience. Fleet fan-out captures each target's outcome independently: success, error, or (for bulk activate/deactivate/delete) indeterminate, when a write's result couldn't be confirmed. One failing node doesn't abort the rest.
  • YAML rule definitions — portable files with structured fields for query, risk, MITRE ATT&CK, incident display, notification, and metadata.
  • Detection-pack install & lifecycle. install-pack applies an entire manifest of rules in one command (--dry-run, --validate-only, --update-if-exists, and Standalone-only --atomic rollback-on-failure). An existing rule found deactivated is reinstalled and reactivated automatically. --prune/--prune-delete deactivate, or (deprecated rules only, backed up) permanently delete, rules a pack no longer wants live.
  • pack-diff previews a pack against one owner's deployment: missing, changed (including drifted active/inactive state), orphaned, deprecated-still-deployed, and ambiguous rules — the same computation install-pack/--prune act on.
  • Fleet topology awareness. discover identifies Search-Head vs. Data-Node roles, plus two mutually-exclusive non-analytics node modes (Guardsix Collector, Raw Syslog Forwarder). Non-analytics nodes are skipped automatically by install-pack and rule operations. A Distributed Guardsix cluster (2+ Search Heads sharing the same Data Nodes) needs --sh-target to disambiguate, but only for the three commands that place a rule (create, update, install-pack) — every other command fans out across the whole cluster and acts wherever the rule actually exists.
  • MITRE ATT&CK resolution on both backends, by different means: Fleet queries its own live catalog; Standalone maps technique IDs through a local CSV file. Both fall back from a sub-technique to its parent (T1059.001 → T1059). See Authentication for details.
  • Pre-mutation backups. Fleet delete (and optionally update) snapshots each rule before modification; restore recreates a rule from any snapshot. Standalone has no backup mechanism.
  • Path-traversal-safe YAML loading — no-follow open + secure_filename, symlink rejection, and containment anchored to the file's own directory (create/update) or the pack's content-root (install-pack), not the current working directory. Rule files can live in a Rules repo checked out anywhere on disk.
  • Automatic token refresh. Standalone re-mints its local JWT within a 5-minute safety margin; Fleet retries POST /refreshToken on a 401 when a secret_key is available.

Standalone vs. Fleet: what differs

Both modes implement the same CLI verbs, but their underlying APIs are not equivalent. This table is the authoritative summary; each item links to where it's covered in full.

Fleet-only commands (hidden from --help under a standalone profile — see Fleet-only commands): discover, get-users, token-check, refresh-token, backup-list, restore.

Shared commands with behavior gaps:

Command Gap
create / update --owner/--owner-id are Fleet-only. A Standalone SIEM API token is bound to a single user, so there is no other user to act on behalf of and owner selection does not apply. YAML assigned_to and manageable_by work on both backends, but each expects a different value type: Standalone takes a name (sent as-is) and rejects an ID; Fleet takes a name (resolved to an ID via Users/IncidentUserGroups) or an ID (used as-is), and rejects an unresolvable name. See create.
create / update MITRE mitre: entries resolve via Fleet's live per-node catalog API on Fleet, but via a local operator-supplied CSV (mitre_map_file) on Standalone — there is no catalog endpoint to query. See Standalone — MITRE ATT&CK attack tags.
create / update minimum_guardsix_version is enforced on Fleet (via MachineInfo) and inert on Standalone, which has no version endpoint. See minimum_guardsix_version.
create / update YAML notification.syslog is honored on Fleet; Standalone has no Syslog Notification API and silently skips it. Both backends support notification.email.
update, delete, install-pack --prune-delete Fleet snapshots the rule first and can restore it; Standalone has no backup mechanism at all (--prune-delete requires --acknowledge-no-backup there). See Pre-mutation backups.
install-pack --atomic (rolls back only this run's created rules, not updates) is Standalone-only. --dry-run-json and --exists-scope are Fleet-only.
pack-diff The VERSION diff category (a minimum_guardsix_version mismatch) only applies on Fleet.
list, get, activate/deactivate, delete, pack-diff --status, --scope, and the Fleet target flags (--pool, --node, --discovery-file) are Fleet-only; Standalone rejects them if passed explicitly, since it has exactly one implicit target.

Other API-level differences, covered where relevant below: repo resolution (Standalone detects Search-Head status live via fetch_repos; Fleet reads a persisted role model from discovery.json — see Distributed Guardsix Repo Resolution), and token lifecycle (Standalone mints a local JWT; Fleet holds a remote Bearer token with secret_key-based refresh — see Authentication).


Requirements

  • Python 3.11 or newer — both g6-alert and g6-alert-core declare requires-python = ">=3.11"; CI tests 3.11, 3.12, and 3.13 on Linux, and 3.11 on Windows.
  • Windows is supported. The chmod 600 permission warning is skipped there, because Windows has no POSIX mode bits. The user profile's access control lists protect files under %USERPROFILE%. Piped or redirected output is UTF-8. To see the CLI's symbols correctly when piping into another PowerShell command, first run [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(). A file redirect keeps them intact in PowerShell 7.4 and later, but not in Windows PowerShell 5.1.
  • Runtime dependencies (installed automatically): requests, urllib3, certifi, PyJWT, click, pyyaml, tabulate, werkzeug, g6-alert-core.

Installation

From PyPI

pip install g6-alert

This installs g6-alert-core automatically as a pinned dependency (g6-alert-core==1.0.2).

From source (development)

Install both packages editable, from the repo root:

pip install -e ./g6_alert_core -e ./g6-alert

This installs g6-alert-core first (editable) and then g6-alert (editable, which satisfies its g6-alert-core requirement from the local wheel).

Development extras (pytest, pytest-cov):

pip install -e "./g6-alert[dev]"

Verify the install:

g6-alert --version

Quick Start

Standalone

# 1. Create a profile
g6-alert config add lab \
  --type standalone \
  --base-url https://guardsix.example.com \
  --subject admin \
  --secret my-signing-secret \
  --default

# 2. List alert rules
g6-alert list

# 3. Create a rule from YAML
g6-alert create rules/my_detection.yaml

# 4. Activate / deactivate by alert ID
g6-alert activate 64abc123def456
g6-alert deactivate 64abc123def456

Fleet

# 1. Create a Fleet profile
g6-alert config add prod \
  --type fleet \
  --fleet-url https://fleet.example.com \
  --access-token <token> \
  --secret-key <secret> \
  --default

# 2. Discover pools and nodes (writes discovery.json)
g6-alert --profile prod discover

# 3. List rules across a pool
g6-alert --profile prod list --pool SOC-Pool

# 4. Install a detection pack
g6-alert --profile prod install-pack day-1 --pool SOC-Pool --owner-id 42

Configuration

Config file

The configuration file is YAML v1 format, stored at:

~/.g6/config.yaml

Override the path via --config <path> or the G6_ALERT_CONFIG environment variable. The file is written with 0o600 permissions (owner-read/write only).

Config, credential (auth.json), and backup-index files are all written atomically, so a crash or a full disk mid-write can't corrupt them. There is no locking between processes, though. Avoid running two g6-alert invocations against the same profile at the same time. A concurrent Fleet token refresh in particular can invalidate a sibling process's still-valid token.

Full v1 schema

version: 1
default_profile: prod-fleet

profiles:
  # Fleet profile
  prod-fleet:
    type: fleet
    fleet_url: https://fleet.example.com
    access_token: <access-token>
    secret_key: <secret-key>
    verify_ssl: true
    discovery_file: ~/.g6/discovery.json   # optional; defaults to ~/.g6/discovery.json
    default_pool: SOC-Pool
    default_node: lp-001   # optional; rarely needed
    auth_file: auth.json       # optional; stores/loads credentials from a JSON file
    pools:                     # optional; per-pool and per-node owner_id overrides
      SOC-Pool:
        owner_id: "42"
        nodes:
          lp-001:
            owner_id: "99"

  # Standalone profile
  lab-standalone:
    type: standalone
    base_url: https://10.45.16.23
    subject: admin
    secret: <signing-secret>
    verify_ssl: false
    auth_file: ~/.g6/lab-auth.json   # optional
    mitre_map_file: ~/.g6/mitre_map.csv   # optional; see "Standalone — MITRE ATT&CK attack tags"

Profile selection order:

  1. --profile NAME CLI flag
  2. G6_PROFILE environment variable
  3. default_profile in the config file

Config management commands

# Create or overwrite a profile
g6-alert config add NAME --type fleet|standalone [options...]

# List all profiles
g6-alert config list

# Show the active profile (secrets masked as abcd…wxyz)
g6-alert config show

# Change the default profile
g6-alert config set-default NAME

# Delete a profile (prompts for confirmation; --yes skips the prompt)
g6-alert config delete NAME [--yes]

The first profile ever added becomes the default automatically; use --default to make a later one the default instead. Deleting the current default profile clears default_profile without picking another automatically — run config set-default afterward.

config add key options:

Flag Backend Description
--type both fleet or standalone (required)
--fleet-url fleet Fleet console base URL
--access-token fleet Initial Bearer token
--secret-key fleet Token refresh secret
--discovery-file fleet Default path for discovery.json
--default-pool fleet Pool used when --pool is omitted
--default-node fleet Node used when --node is omitted
--base-url standalone Guardsix instance URL
--subject standalone Auth subject (username)
--secret standalone JWT signing secret
--mitre-map-file standalone Path to the MITRE technique->hash mapping CSV
--verify-ssl / --no-verify-ssl both TLS verification (default: on)
--auth-file both Path to external auth.json credential file
--default both Make this the default profile
--force both Overwrite an existing profile

config add --verify-ssl/--no-verify-ssl sets the value stored in the profile. A separate, global --tls-verify/--no-tls-verify flag is available on every command (e.g. g6-alert --no-tls-verify list ...) to override TLS verification for a single run without editing the profile. Resolution order: global CLI flag > G6_VERIFY_SSL (fleet also checks G6_FLEET_VERIFY_SSL first) > profile value.


Authentication

Credential precedence

Credentials are resolved in this order (most specific wins):

explicit override (programmatic)  >  environment variable (G6_*)  >  profile value (config.yaml)

The CLI exposes no per-credential flags, so the "explicit override" tier applies to library usage — arguments passed to build_client(..., overrides=...) or a backend SDK constructor. For CLI users, credentials come from the G6_* environment variables or the active profile.

A profile can instead point at an auth.json via auth_file; when set and loadable, that file supplies the credentials, superseding the profile's inline fields (any field it omits falls back to the matching G6_* env var).

Standalone — local self-signed JWT

  • No server token endpoint — tokens are generated locally via PyJWT.

  • Algorithms: HS256, HS384, HS512 (default HS256). RS/ES variants are rejected to prevent algorithm-confusion attacks.

  • Token TTL: 1 hour. Re-minted automatically when less than 5 minutes remain on the current token, or immediately on a 401 response.

  • Credential fields: base_url, subject, secret.

  • Environment variables:

    Variable Field
    G6_BASE_URL Base URL
    G6_SUBJECT Subject (username)
    G6_SECRET Signing secret
    G6_VERIFY_SSL TLS verification
    G6_MITRE_MAP_FILE Path to the MITRE technique->hash mapping CSV

    G6_VERIFY_SSL accepts true, on, yes, or 1 (case-insensitive) to enable verification; anything else disables it. It overrides the profile's verify_ssl value.

  • Security notes: a warning is printed if the base URL uses plain HTTP (credentials would be transmitted in cleartext); credential files are expected to be 0o600.

Standalone — MITRE ATT&CK attack tags

The standalone server has no MITRE catalog endpoint (unlike Fleet, which resolves attack tags via a live per-pool/node lookup — see below). Instead, a CSV maps each MITRE technique ID to the hash the server's own taxonomy.attack_tag_hashes field expects:

# MITRE ATT&CK Enterprise v18 (bundled with g6-alert-core)
hash,attack_id,attack_tag,attack_category
94711e5d399c28939c688f086b7faf85,"T1595","Active Scanning","Reconnaissance"
a846061737d21c675dcda4c7f087bc83,"T1595.001","Scanning IP Blocks","Reconnaissance"

A copy of this CSV, generated from MITRE ATT&CK Enterprise v18, ships with the tool, so attack tags resolve out of the box after install. Resolution order:

  1. --mitre-map-file / G6_MITRE_MAP_FILE / profile.mitre_map_file, if set — any explicit absolute path is accepted (not restricted to CWD or ~/.g6); only a relative path containing a .. segment is rejected.
  2. ~/.g6/mitre_map.csv (co-located with the config file), if present — drop your own file here to override the bundled copy without touching config.
  3. Otherwise, the CSV bundled with the package.
  • Resolution mirrors Fleet's own sub-technique fallback: an exact attack_id match wins; otherwise a sub-technique (T1595.001) falls back to its parent (T1595) if the parent is in the CSV.
  • A rule with no mitre: section never touches the CSV. A missing/misconfigured CSV, or an unresolvable technique ID, degrades gracefully — the create/update still succeeds, and the CLI prints a ⚠️ warning naming the technique instead of failing the operation.

Fleet — remote Bearer token with refresh

  • Header: Authorization: Bearer <access_token> on every request.

  • Refresh: POST {fleet_url}/refreshToken with body {"secretKey": "<secret_key>"}. Returns {"tokenDetails": {"token": ..., "secretKey": ...}}. A 401 with no secret_key present surfaces immediately to the caller.

  • Token age lifecycle (surfaced by token-check):

    Age Status
    < 4 h ok
    4 – 8 h monitor
    ≥ 8 h expired — renew with refresh-token (also auto-refreshes on a 401)
  • Environment variables:

    Variable Field
    G6_FLEET_URL Fleet console URL
    G6_FLEET_ACCESS_TOKEN Bearer token
    G6_FLEET_SECRET_KEY Refresh secret
    G6_FLEET_VERIFY_SSL TLS verification
    G6_VERIFY_SSL TLS verification (checked if G6_FLEET_VERIFY_SSL is unset)
  • Security notes: a warning is printed if the Fleet URL uses plain HTTP (the access token would be transmitted in cleartext); credential files are expected to be 0o600.


auth.json — Credential File Format

auth.json is an alternative to inline profile credentials — instead of putting fleet_url/access_token/secret_key (or base_url/subject/secret) directly in a profile, point the profile at a JSON file that holds them. A profile opts in via its auth_file: field (or --auth-file when creating one with config add); there is no environment variable for the file's path. Default filename: auth.json. Both backends write it atomically at mode 0o600, and Fleet and Standalone use different schemas — pick the one matching your profile's type.

Fleet

{
  "fleet_url": "https://fleet.example.com",
  "access_token": "eyJhbGciO...",
  "secret_key": "ab1979e1-1234-4dff-9725-b900c43502d3",
  "token_created_at": "2026-07-10T09:00:00.000000",
  "token_last_updated": "2026-07-14T03:15:00.000000"
}
Field Required Written by Description
fleet_url yes you Fleet console URL
access_token yes you / SDK Bearer token
secret_key no you / SDK Refresh secret for refresh-token / auto-refresh-on-401
token_created_at — SDK-managed ISO-8601; set only when access_token changes; drives token-check's age lifecycle
token_last_updated — SDK-managed ISO-8601; rewritten on every save

Loading fails with AuthenticationError unless both fleet_url and access_token are present. token_created_at/token_last_updated are managed entirely by the SDK — hand-written files can omit them.

Standalone

{
  "base_url": "https://standalone.example.com",
  "subject": "svc-alert-sdk",
  "secret": "a-strong-signing-secret",
  "created_at": "2026-07-14T03:15:00.000000"
}
Field Required Written by Description
base_url yes you Standalone server base URL
subject yes you JWT subject (username)
secret yes you JWT signing secret
created_at — SDK-managed ISO-8601, set on save

Loading fails with AuthenticationError unless base_url, subject, and secret are all present. Unlike Fleet, no token is stored — Standalone mints a short-lived JWT locally at runtime and keeps it in memory only, so there's nothing to refresh or persist beyond the signing secret.

File permissions & source precedence

Both backends write auth.json atomically (tempfile + os.chmod(0o600) + os.replace) so a crash mid-write can't corrupt it or leave it world-readable. On load, a warning (not a failure) is printed if the file is already group- or world-readable.

When a profile sets auth_file and the file loads successfully, its credentials are authoritative for that profile — they supersede the profile's inline fields. Any field the file doesn't define falls back to the matching G6_* environment variable (see Credential precedence). To avoid surprises, pick one credential source per profile — either inline fields, or auth_file — rather than mixing both.


CLI Usage

Global options

g6-alert [OPTIONS] COMMAND [ARGS]...

Options:
  --profile TEXT                  Config profile name (else G6_PROFILE / default_profile)
  --config PATH                   Path to config file (else ~/.g6/config.yaml)
  --tls-verify / --no-tls-verify  Verify TLS certificates for this run (else
                                   G6_VERIFY_SSL / profile)
  -v, --verbose                   Enable verbose/debug logging
  --version                       Show version and exit
  --help                          Show this message and exit

--tls-verify/--no-tls-verify, like --profile/--config/-v, works before or after the subcommand name (e.g. both g6-alert --no-tls-verify list and g6-alert list --no-tls-verify).

Fleet-only commands (discover, get-users, backup-list, restore, token-check, refresh-token) are hidden from --help when the active profile is standalone.

Most commands accept these Fleet target flags:

Flag Description
--pool TEXT Pool name or UUID (repeatable)
--node TEXT Node identifier (repeatable)
--discovery-file PATH Path to discovery.json

They only make sense for Fleet. Under a standalone profile the CLI rejects any of them if explicitly passed on the command line, with a usage error — they are not silently ignored. The same applies to every other Fleet-only flag documented below (--owner, --owner-id, --scope, --status, --no-backup, --sh-target, --exists-scope, --dry-run-json).

--sh-target TEXT (repeatable/comma-separated) disambiguates a multi-Search-Head Distributed Guardsix cluster. It's accepted, and required when a cluster is ambiguous, only by create, update, and install-pack — the three commands that place a new rule and so need to know which Search Head should host it. Every other Fleet command resolves targets across all Search Heads in the cluster with no flag needed, since there's no placement decision to make when operating on rules that already exist.


Shared commands

list — List alert rules

g6-alert list [OPTIONS]

Options:
  --search TEXT         Filter by name/content
  --status TEXT         Filter by status (Fleet)
  --risk [critical|high|medium|low]
  --active [all|true|false]   Default: all
  --scope TEXT          my / shared / vendor / used (Fleet)
  --json-output         Print full JSON instead of a table
  --show-owner          Add an Owner column (Fleet: resolves username from owner ID;
                        Standalone: shows the inline user field)
  [target flags]
# List all rules in a pool
g6-alert --profile prod list --pool SOC-Pool

# List only high-risk active rules
g6-alert list --risk high --active true

# Search by name, full JSON output
g6-alert list --search "PowerShell" --json-output

get ALERT_ID — Show one alert rule

g6-alert get ALERT_ID [--scope TEXT] [--json-output] [target flags]
g6-alert get 64abc123def456
g6-alert --profile prod get 64abc123def456 --pool SOC-Pool --json-output

create YAML_FILE — Create a rule from a YAML file

g6-alert create YAML_FILE [--owner-id TEXT] [--owner TEXT] [--repo TEXT]... \
         [--sh-target TEXT]... [--update-if-exists] [--dry-run] [target flags]

--owner resolves a username to an owner ID per node (Fleet; mutually exclusive with --owner-id). --sh-target picks which Search Head(s) should host the rule in a multi-SH Distributed Guardsix cluster (Fleet; repeatable/comma-separated) — required only when 2+ Search Heads of the same cluster are targeted.

--update-if-exists only overwrites an alert whose rule_id tag confirms it's the same rule. A same-named alert with no matching rule_id tag is never overwritten; create attempts a fresh create instead, which the SIEM's own per-owner name uniqueness check resolves to a clean skip if the name is already taken. The skip message explains why (identity_warnings).

A rule YAML may also declare assigned_to: <username or ID> (the one user who can re-assign, comment on, view, and resolve incidents this rule generates) and manageable_by: [<group name or ID>, ...] (incident user groups whose members can re-assign, comment on, and view, but not resolve, incidents). Both are optional. A value is either a name or an already-resolved ID (a 24-character lowercase hex string, e.g. 5a467b3dd8aaa461c3139038), but which one is accepted depends on the backend.

On Fleet, a name is resolved live the same way --owner resolves a username, and an ID is used as-is with no lookup. An unresolvable name fails the create/update closed (raises) rather than silently creating a rule with a gap in who can act on its incidents. A pack manifest may set both pack-wide via pack.assigned_to/pack.manageable_by, see install-pack [PACK_ID].

On Standalone, only a name is accepted. There's no Users/IncidentUserGroups lookup endpoint to convert an ID into a name, so an ID value fails closed (raises) rather than being sent to the server unresolved. A name is sent as-is. If either field is absent, the incident assignee defaults to the profile's auth subject and no incident user groups are granted visibility.

A rule's notification.email section is honored on both backends. A notification.syslog section is Fleet-only — Standalone has no Syslog Notification API and silently skips it. After deploying rules to a Standalone SIEM, use the SIEM UI's bulk syslog notification update feature to apply the required syslog notification template to them. See Rule YAML — notification.syslog for how the syslog message body is built.

A rule YAML may also declare minimum_guardsix_version: <X.Y.Z> — Fleet blocks (raises) the create if the target node's actual version doesn't meet it, or can't be determined at all. Fleet-only; see Rule YAML — minimum_guardsix_version.

YAML_FILE may live anywhere on disk — e.g. a Rules repo checked out separately from this SDK/CLI — on both backends; path-safety containment anchors to the file's own directory, not the current working directory.

g6-alert create rules/my_detection.yaml
g6-alert --profile prod create rules/my_detection.yaml \
         --pool SOC-Pool --owner-id 42

update ALERT_ID YAML_FILE — Update an existing rule

g6-alert update ALERT_ID YAML_FILE [--owner-id TEXT] [--owner TEXT] \
         [--repo TEXT]... [--sh-target TEXT]... [--dry-run] [target flags]

--sh-target picks which Search Head(s) should host the rule in a multi-SH Distributed Guardsix cluster (Fleet; repeatable/comma-separated) — required only when 2+ Search Heads of the same cluster are targeted. Same minimum_guardsix_version gate as create. YAML_FILE is subject to the same anywhere-on-disk path handling described under create above.

activate / deactivate — Enable or disable rules

g6-alert activate [TOKENS...] [OPTIONS] [target flags]
g6-alert deactivate [TOKENS...] [OPTIONS] [target flags]

Options:
  --by-name            Match by rule name instead of ID
  --by-pack-tags       Treat positional arguments as pack tags; ALL must match
                       (AND logic). Mutually exclusive with --by-name.
  --owner-id TEXT      Alert owner ID (Fleet; required with --by-name/--by-pack-tags)
  --owner TEXT         Alert owner username (Fleet; resolved to owner_id per node;
                       required with --by-name/--by-pack-tags)
  --scope TEXT         Scope for name/tag resolution (Fleet)
  --yes                Skip confirmation prompt
  --dry-run            Preview without applying

--by-name and --by-pack-tags work on both backends (Standalone fetches each alert's full detail to match names/tags client-side); --scope, --owner-id, and --owner are Fleet-only. Fleet requires one of --owner-id/--owner whenever --by-name/--by-pack-tags is used, so a name or tag match never crosses into another user's rules.

With --by-pack-tags, if the given tokens resolve to an empty tag set (e.g. blank/ whitespace arguments), the command is rejected with a usage error before any network call — an empty tag set would otherwise match every alert.

# By alert ID
g6-alert activate 64abc123def456

# By name
g6-alert deactivate "AADInternals PowerShell Detection" --by-name

# By pack tags (Fleet: deactivate all rules tagged day-1 AND customerA, owned by 42)
g6-alert --profile prod deactivate day-1 customerA \
         --by-pack-tags --pool SOC-Pool --owner-id 42 --yes

delete [TOKENS...] — Permanently delete rules

g6-alert delete [TOKENS...] [OPTIONS] [target flags]

Options:
  --by-name            Match by rule name
  --by-pack-tags       Match by pack tag; ALL must match (AND logic)
  --owner-id TEXT      Alert owner ID (Fleet; required with --by-name/--by-pack-tags)
  --owner TEXT         Alert owner username (Fleet; resolved to owner_id per node;
                       required with --by-name/--by-pack-tags)
  --scope TEXT         Scope for name/tag resolution (Fleet)
  --yes                Skip confirmation
  --no-backup          Skip pre-delete backup (Fleet only)
  --max-delete N       Safety cap: abort before deleting anything if the
                       matched/given alert count exceeds N
  --dry-run            Preview without deleting

As with activate/deactivate, --by-name and --by-pack-tags work on both backends (--scope, --owner-id, and --owner are Fleet-only, and one of --owner-id/--owner is required alongside them there), and an empty resolved --by-pack-tags set is rejected with a usage error before any network call.

# Preview, then confirm delete
g6-alert --profile prod delete 64abc123def456 --pool SOC-Pool --dry-run
g6-alert --profile prod delete 64abc123def456 --pool SOC-Pool --yes

On Fleet, a backup snapshot is created before each deletion unless --no-backup is given. Use backup-list and restore to recover. Standalone has no backup mechanism, so delete there is not recoverable.

Deleting an alert that no longer exists succeeds as a no-op: the command reports it as already gone rather than as an error, so delete is safe to rerun.

pack-diff [PACK_ID] — Preview a pack vs. a deployment

g6-alert pack-diff [PACK_ID] [OPTIONS] [target flags]

Options:
  --pack TEXT          Pack ID (alternative to positional arg)
  --packs-dir PATH     Root of packs directory (default: Packs/)
  --content-root PATH  Root that manifest rule paths resolve against
                       (default: the parent of --packs-dir)
  --owner-id TEXT      Owner to diff against (Fleet; mandatory, mutually
                       exclusive with --owner)
  --owner TEXT         Owner username to diff against (Fleet; mandatory)
  --scope TEXT         Scope used to read deployed alerts (Fleet)
  --json-output        Emit the diff as JSON instead of tables
g6-alert pack-diff day-1
g6-alert --profile prod pack-diff day-1 --pool SOC-Pool --owner-id 42

Strictly read-only: never creates, updates, deletes, or deactivates anything. Fleet allows multiple owners to each run their own independent copy of a rule, so the diff is always scoped to a single owner (the same unit of work install-pack uses). For each target it reports:

  • PRESENT — in the pack and deployed under this owner, matching.
  • MISSING — in the pack, not deployed under this owner.
  • CHANGED — deployed but differs from the pack. Causes include name/description/risk/query drift, a name match whose rule_id diverges (an unmanaged rule squatting the name), a drifted active state, or a repo drift, e.g.: CHANGED • Activity Related to NTDS Domain Hash Retrieval (id=...) changed: active (currently INACTIVE — pack expects it active). install-pack's existing-rule reconciliation auto-fixes a drifted active state, but only for a confirmed rule_id match. A name-only match (no rule_id tag on the deployed alert) is never reinstalled or reactivated automatically; it's treated as a fresh rule instead, the same as no match at all. Reconciliation does not act on a repo-only change — an already-deployed active rule is always skipped there unless --update-if-exists is passed. Repo is only compared when the pack pins one explicitly (the manifest rule entry's own repo:, or failing that the rule YAML file's own top-level repo:); a rule that never pins a repo is never flagged just because the live repo catalog or the deployed alert's auto-fetched repos drift over time.
  • ORPHANED — deployed under this owner, carries this pack's tags, no longer in the pack manifest.
  • DEPRECATED — the pack marks this rule status: deprecated and it's still deployed under this owner (scheduled for removal, not yet cleaned up). install-pack --prune/--prune-delete only acts on a confirmed rule_id match; a name-only match is held back and reported as NEEDS REVIEW instead, never deactivated or deleted automatically.
  • AMBIGUOUS — this owner has 2+ alerts sharing a rule_id; none are classified until the duplicates are resolved.
  • VERSION — Fleet only. The rule declares minimum_guardsix_version and this target's Guardsix version doesn't meet it, or couldn't be determined. This is a flag, not a block, and can appear alongside any other category above, since pack-diff never blocks anything. See Rule YAML — minimum_guardsix_version.

Use install-pack --prune/--prune-delete to deactivate or delete DEPRECATED/ ORPHANED rules, or delete --by-pack-tags to remove them yourself.

install-pack [PACK_ID] — Install a detection pack

g6-alert install-pack [PACK_ID] [OPTIONS] [target flags]

Options:
  --pack TEXT           Pack ID (alternative to positional arg)
  --packs-dir PATH      Root of packs directory (default: Packs/)
  --content-root PATH   Root that manifest rule paths resolve against
                        (default: the parent of --packs-dir)
  --owner-id TEXT       Rule owner ID (Fleet; mutually exclusive with --owner)
  --owner TEXT          Rule owner username, resolved to an owner ID per node
                        (Fleet; mutually exclusive with --owner-id)
  --repo TEXT           Repo address (repeatable; both backends)
  --sh-target TEXT      Restrict installation to specific Search Head(s) in a
                        multi-SH Distributed Guardsix cluster (Fleet; repeatable)
  --exists-scope TEXT   Scope used when checking if a rule already exists (Fleet)
  --update-if-exists    Update instead of skipping existing rules
  --dry-run             Preview CREATE/UPDATE/SKIP against the live deployment
                        (server-assisted — schema validation still runs, so
                        a dry-run surfaces the same errors a real apply would)
  --validate-only       Validate the manifest without installing
  --offline             Fully offline dry-run — no API calls at all, including
                        the existence check, so it can only preview CREATE
                        (implies --dry-run)
  --dry-run-json        Output dry-run plan as JSON (Fleet)
  --atomic              Roll back this run's created alerts if any rule
                        fails (Standalone only)
  --prune               After installing, deactivate (never delete)
                        deprecated-and-still-deployed and orphaned
                        (removed-from-manifest) rules. Prompts unless --yes.
  --prune-delete        Escalate --prune's action on deprecated rules to a
                        permanent delete (requires --prune)
  --no-backup           Skip the pre-delete backup for --prune-delete
                        (Fleet only)
  --acknowledge-no-backup
                        Required with --prune-delete on Standalone, which has
                        no backup mechanism
  --yes                 Skip the --prune confirmation prompt

--packs-dir isn't restricted to the current working directory: the manifest can be anywhere on disk. Every rule path: in the manifest resolves against a content root, which defaults to the parent of --packs-dir — so a pack and the Rules it references can live together in a separately released repo checked out anywhere on disk, as long as they share that parent. Pass --content-root to put --packs-dir and the rule tree in separate locations; manifest path: entries stay unchanged, only the directory they resolve against moves.

# Default: Packs and Rules share a parent
g6-alert install-pack day-1 --packs-dir /srv/g6-content/Packs
# rules resolve at /srv/g6-content/Rules/...

# Packs and Rules in separate trees
g6-alert install-pack day-1 --packs-dir /srv/packs --content-root /srv/detection-content
# rules resolve at /srv/detection-content/Rules/...
# Standalone
g6-alert install-pack day-1

# Standalone — rolls back this run's created rules if a later rule fails (updates aren't rolled back)
g6-alert install-pack day-1 --atomic

# Fleet — dry run first (server-assisted: reports CREATE/UPDATE/SKIP per rule
# against what's actually deployed; add --offline for a fully disconnected
# preview, which can only ever report CREATE)
g6-alert --profile prod install-pack day-1 --pool SOC-Pool --dry-run
g6-alert --profile prod install-pack day-1 --pool SOC-Pool --owner-id 42

# Preview, then deactivate deprecated/orphaned rules no longer in the pack
g6-alert --profile prod install-pack day-1 --pool SOC-Pool --owner-id 42 --prune --dry-run
g6-alert --profile prod install-pack day-1 --pool SOC-Pool --owner-id 42 --prune --yes

# Permanently delete just the deprecated subset (Fleet backs up automatically)
g6-alert --profile prod install-pack day-1 --pool SOC-Pool --owner-id 42 \
  --prune --prune-delete --yes

# Same, Standalone (no backup mechanism — must be explicitly acknowledged)
g6-alert install-pack day-1 --prune --prune-delete --acknowledge-no-backup --yes

The pack manifest (Packs/<pack-id>/manifest.yaml) lists rules by relative path. Fleet auto-discovers repos, skips Data Nodes/Guardsix Collectors/Raw Syslog Forwarders, and injects pack_tags metadata. Fleet also skips (per rule/target pair, with a visible skipped (requires Guardsix >= X (target is Y)) line) any rule whose minimum_guardsix_version isn't met by that target — see Rule YAML — minimum_guardsix_version. Use pack-diff beforehand to preview exactly what any of the below will do.

Manifest per-rule fields. Each entry in the manifest's rules: list supports these fields:

Field Required Type Purpose
path yes string Relative path to the rule YAML, resolved against the content root.
repo no list Repo name(s) to place the rule on. Must be a YAML sequence — write repo: [Windows], not repo: Windows.
rule_id no string Stable ID used to match against deployed alerts. Falls back to the deployed alert's own rule_id, then the rule path, when omitted.
status no (default active) string Lifecycle state. Only active or deprecated are allowed. Full removal is expressed by deleting the entry, not a third status.
added_in no string Pack version the rule was introduced in. If the key is present it must be non-empty; omit it entirely rather than leaving it blank.
deprecated_in yes when status: deprecated string Pack version the rule was deprecated in.
deprecated_reason yes when status: deprecated string Reason for deprecation, surfaced in --prune/--prune-delete output.

status: deprecated is fail-closed: it's rejected unless both deprecated_in and deprecated_reason are present, so a rule never silently disappears from a pack without an audited reason. Deprecated-and-still-deployed rules are what pack-diff's DEPRECATED category and install-pack --prune/--prune-delete act on. Pack-wide fields (assigned_to/manageable_by) live in the top-level pack: section, not per rule — see below.

Existing-rule reconciliation (no flag needed). Matching is by the rule's stable rule_id. An existing rule that's still active is skipped (skipped '<name>' (exists, active)). An existing rule found inactive is automatically reinstalled from the on-disk YAML and reactivated, since a plain create-or-skip install would otherwise never revisit it, but only for a confirmed rule_id match; a name-only match (no rule_id tag on the deployed alert) is never reinstalled or reactivated automatically, and a fresh create is attempted instead. This reinstall-and-reactivate path can't be triggered by --update-if-exists alone: the update payload built from the YAML carries no active/enabled field, so activation is always a separate API call. It's reported as one line, reinstalled '<name>' from disk and activated (was inactive), and counted as a success, not a skip. --dry-run's prediction (would CREATE/would UPDATE/would SKIP) uses this same rule_id matching and inactive-reinstalls-as-update logic, so the preview always matches what a real apply does; --offline can't check what's deployed and always previews CREATE. --update-if-exists remains available to force-refresh an already-active existing rule's definition from disk.

Pack-wide assigned_to/manageable_by (Fleet only). A manifest's pack: section may set assigned_to: <username or ID> and manageable_by: [<group name or ID>, ...], the same fields a rule YAML can declare individually (see create YAML_FILE). When set, the pack's value is resolved once per target and applied to every rule in the pack, taking precedence over that rule's own YAML value. An empty/omitted pack value (the default) sets no pack-wide default, so each rule's own value, if any, is used instead. Resolution is fail-closed like the per-rule fields: an unresolvable pack-level name fails every rule for that target rather than silently installing with a gap in who can act on the resulting incidents.

--atomic (Standalone only). If any rule in the pack fails after one or more rules have already been created, the CLI deletes the rules created by this run before exiting; rules that were skipped (already existed) or updated are left untouched. It never triggers under --dry-run or --validate-only. If a compensating delete itself fails, the CLI prints Rollback FAILED for <id>: ... and a final N rollback(s) failed — manual cleanup may be required warning instead of failing silently.

Fleet's install-pack doesn't support --atomic: a single run fans out across every resolved pool/node, so "roll back this run's creates" is ambiguous once some targets have succeeded and others haven't, and a timed-out/unconfirmed async write leaves the SDK unsure whether the rule was actually created, so there's no reliable ID to roll back by. Fleet's safety net is the pre-mutation backup + restore flow instead (delete snapshots each rule before removing it).

--prune. Removing a rule from the pack manifest and re-running install-pack, even with --update-if-exists, does not touch the already-deployed copy on its own — the install loop only ever iterates the manifest's own rules. --prune closes that gap: after installing, it deactivates (never deletes) two categories — status: deprecated rules still deployed, and rules dropped from the manifest entirely but still carrying the pack's tags (orphaned). It prompts for confirmation unless --yes, --dry-run prints the candidates without acting, and it's skipped entirely under --offline/--validate-only.

--prune-delete. --prune only deactivates. delete --by-pack-tags can't substitute for permanent cleanup here, since every rule in a pack shares the same tags and a tag-based delete would match the entire pack, not just the deprecated rules. --prune-delete escalates only the deprecated-and-still-deployed subset to a permanent delete, reusing the same backup+delete path delete uses. Orphaned (removed-from-manifest) rules are always just deactivated, never deleted here, since that's a weaker signal than an explicit status: deprecated. Requires --prune. On Fleet each deleted rule is backed up first (skip with --no-backup; restore with backup-list/restore); Standalone has no backup mechanism at all, so --prune-delete there additionally requires --acknowledge-no-backup or fails closed with a usage error before any API calls.


Fleet-only commands

These commands are hidden from --help under a standalone profile.

discover — Map pools and nodes

g6-alert [--profile NAME] discover [-o OUTPUT]

Queries the Fleet REST API to enumerate pools and nodes, then probes each node's DistributedLogpoints endpoint to label Search-Head and Data-Node roles, its SystemSettingsModesOfOperation/RawSyslogForwarder endpoints to flag the two mutually-exclusive non-analytics node modes (Guardsix Collector, Raw Syslog Forwarder), and its MachineInfo endpoint for the node's running Guardsix version. A node's mode is shown as a (Guardsix Collector)/(Raw Syslog Forwarder) suffix, followed by a [vX.Y.Z] version suffix when it could be determined (e.g. - sh1 | host-sh1 [v7.8.0.2]). Neither mode changes the node's Search-Head/Data-Node/Standalone grouping in the printed output — the topology of which Data Nodes or Search Heads a given collector/forwarder feeds isn't tracked, only the mode itself. Results are written to the shared discovery cache at ~/.g6/discovery.json (permissions 0o600), keyed by profile name so multiple profiles coexist in one file.

Version display is Fleet-only; Standalone has no equivalent endpoint.

Output path resolution: -o PATH > profile.discovery_file > ~/.g6/discovery.json.

# Use the default path (~/.g6/discovery.json)
g6-alert --profile prod discover

# Override the output path
g6-alert --profile prod discover -o /tmp/discovery.json

See discovery.json for the output schema.

get-users — List Fleet users (find owner IDs)

g6-alert --profile prod get-users --pool SOC-Pool
g6-alert --profile prod get-users --pool SOC-Pool --json-output
g6-alert --profile prod get-users --pool SOC-Pool --config-only  # prints owner_id snippet

Default output is a table (Pool / Node / Node ID / Username / User ID). --json-output emits JSON instead; --config-only prints a YAML owner_id snippet for pasting into a profile's pools: section.

token-check / refresh-token — Token lifecycle management

g6-alert --profile prod token-check    # reports token age and recommendation
g6-alert --profile prod refresh-token  # refreshes using secret_key

backup-list — List local rule backups

g6-alert --profile prod backup-list [--pool POOL] [--node LP] [--alert-id ID] [--json-output]

restore — Restore a rule from a backup

g6-alert --profile prod restore \
  --backup-file ~/.g6/backups/SOC-Pool/lp-001/64abc123def456/2026-06-01_12-00-00_123456.yaml \
  --pool SOC-Pool [--alert-id ID] --yes

Backups are written under ~/.g6/backups/<pool>/<node>/<alert_id>/<timestamp>.yaml (co-located with the config file; backup-list reads this same tree). --backup-file isn't restricted to that tree, so restoring a copy brought in from another host is supported, but a symlinked backup file is refused (guards against a symlink swap between validation and read).

Restores by updating the rule if it still exists, or creating it if it was deleted. Use --dry-run to preview without applying.

Restore reconstructs the rule from the backed-up alert's own API row, so every field comes back exactly as it was: query, risk, condition, rule_status, pack tags, MITRE attack_tag mappings, the incident data view's Jinja template, assigned_to/ manageable_by, and email/syslog notification config, including the exact rendered message templates.

A create, update, or delete can fail after its pre-mutation backup was already written, leaving the rule in whatever state that attempt left it. The backup from just before that mutation is exactly what restore needs to put it back.


Config management

g6-alert config add NAME [--type fleet|standalone] [options...]
g6-alert config list
g6-alert config show
g6-alert config set-default NAME
g6-alert config delete NAME [--yes]

See Configuration for full option tables.


Programmatic Usage

Import and build a client via the factory (recommended) or construct backend SDKs directly for advanced use.

Standalone (Library)

from g6_alert.config import Profile
from g6_alert.factory import build_client
from g6_alert_core import AlertSDKError, AlertNotFoundError

profile = Profile(
    name="lab",
    type="standalone",
    base_url="https://guardsix.example.com",
    subject="admin",
    secret="my-signing-secret",
)
client = build_client(profile)  # -> AlertClient backed by StandaloneTransport

# List all rules
results = client.list_alerts()
for r in results:
    if r.ok:
        for rule in r.value:
            print(rule["name"], rule.get("status"))

# Create a rule (path-traversal-safe; return value contains server response)
results = client.create_alert("rules/my_detection.yaml")
assert results[0].ok, results[0].error

# Get by ID (raises AlertNotFoundError if missing)
try:
    results = client.get_alert("64abc123def456")
    detail = results[0].value
except AlertNotFoundError:
    print("Rule not found")

# Activate / deactivate / delete (by list of IDs)
client.activate(["64abc123def456"])
client.deactivate(["64abc123def456"])
client.delete(["64abc123def456"])

client.close()

Fleet (Library)

from g6_alert.config import Profile
from g6_alert.factory import build_client

profile = Profile(
    name="prod",
    type="fleet",
    fleet_url="https://fleet.example.com",
    access_token="<access-token>",
    secret_key="<refresh-secret>",
    discovery_file="~/.g6/discovery.json",
)
client = build_client(profile)  # -> AlertClient backed by FleetTransport

# List rules across all nodes in a pool
results = client.list_alerts(pools=["SOC-Pool"])
for r in results:
    print(f"{r.target.label}: {'ok' if r.ok else r.error}")
    if r.ok:
        for rule in r.value:
            print(f"  {rule['name']} [{rule.get('status')}]")

# Create — the async Fleet "order" is polled automatically via await_result
results = client.create_alert(
    "rules/my_detection.yaml",
    pools=["SOC-Pool"],
    owner_id="42",
)

# Filter to specific nodes
results = client.list_alerts(
    pools=["SOC-Pool"],
    nodes=["lp-001"],
    search="PowerShell",
    risk="high",
)

client.close()

Backup is a CLI-only safety net. The g6-alert CLI wraps a pre-mutation backup around update, delete, create --update-if-exists, and install-pack's overwrite/prune-delete paths (see the restore command under CLI Usage above). Calling create_alert/update_alert/delete_alert/ bulk_delete_alerts directly through this library, as in the example above, does not create a backup — that protection only exists at the CLI layer. If you're overwriting or deleting alerts programmatically and want the same safety net, back up the existing alert yourself first (g6_alert.backends.fleet.backup.AlertBackupManager) before calling these methods.

Per-Target Results

Every AlertClient method returns List[TargetResult]. Check each result individually:

results = client.create_alert("rules/my_detection.yaml", pools=["SOC-Pool"])
for r in results:
    if r.ok:
        print(f"[{r.target.label}] created: {r.value}")
    else:
        print(f"[{r.target.label}] FAILED: {r.error}")

API Reference

AlertClient

Import: from g6_alert_core.client import AlertClient
Factory: from g6_alert.factory import build_client

All methods return List[TargetResult]. Fleet calls fan out across resolved targets; standalone always returns a single-element list.

Method Key parameters Description
list_alerts pools, nodes, discovery_file, search, status, risk, active, scope, limit List rules with optional filters
get_alert alert_id, pools, nodes, discovery_file, scope Fetch one rule by ID
create_alert yaml_file, pools, nodes, discovery_file, sh_targets, repos, owner_id, owner, update_if_exists Create from YAML (order-polled on Fleet); sh_targets disambiguates a multi-SH Distributed Guardsix cluster and is enforced (required if ambiguous)
update_alert alert_id, yaml_file, pools, nodes, discovery_file, sh_targets, repos, owner_id, owner Update an existing rule; sh_targets enforced the same way as create_alert
activate alert_ids: List[str], target flags Enable rules
deactivate alert_ids: List[str], target flags Disable rules
delete alert_ids: List[str], target flags Delete rules
resolve_targets pools, nodes, discovery_file, sh_targets, enforce_sh_disambiguation Enumerate Target objects without operating; enforce_sh_disambiguation (default False) makes an unresolved multi-SH cluster fatal — only create_alert/update_alert pass True, so every other caller fans out across all Search Heads in the cluster instead of raising
supports(capability) "discover", "backup", "mitre", "multi_target" Check backend capability
capabilities (property) — frozenset of supported capability strings
close — Release underlying HTTP sessions

Target and TargetResult

Target — a frozen dataclass identifying one destination:

Field Type Description
pool_uuid Optional[str] Pool UUID (Fleet); None for standalone
node_id Optional[str] Node identifier (Fleet); None for standalone
pool_name Optional[str] Human-readable pool name (Fleet)
node_name Optional[str] Human-readable node name (Fleet)
label str Display string (e.g. "SOC-Pool/lp-001" or "default")
search_head_names frozenset Every Search Head hostname in this target's pool (Fleet; empty for standalone) — lets a mutating verb exclude those repos without a second discovery lookup
is_standalone property True when pool_uuid is None
display_label property "pool/Name (id)" when the node name is known, else falls back to label

TargetResult — outcome for one target:

Field Type Description
target Target The target this result is for
ok bool True if the operation succeeded
value Any Response payload on success (list/dict/etc.)
error Optional[str] Error message string on failure
exc_type Optional[str] Exception class name on failure (e.g. "APIError")
status_code Optional[int] HTTP status code on failure, when available

Fleet capabilities:

client.supports("multi_target")  # True — fan-out across pools
client.supports("backup")        # True — pre-delete snapshots
client.supports("mitre")         # True — ATT&CK tag resolution
client.supports("discover")      # True — discovery.json generation

Standalone returns False for all of the above.

Backend SDKs

For advanced or direct use, the underlying SDK classes are importable:

from g6_alert.backends.standalone import AlertSDK
from g6_alert.backends.fleet import FleetAlertSDK

AlertSDK (standalone) key methods

Method Returns Notes
list_alerts(search, enabled, limit) List[Dict] Auto-paginates internally; always returns the complete result set
get_alert(alert_id) Dict Raises AlertNotFoundError
create_alert(yaml_path, yaml_data, check_exists, extra_metadata, repos, update_if_exists) Dict
update_alert(alert_id, yaml_path, yaml_data, repos, extra_metadata) Dict
activate_alert(alert_ids) Dict
deactivate_alert(alert_ids) Dict
delete_alert(alert_ids) Dict
fetch_repos(notify_search_head) List[str] Excludes local loopback repos on Search Head
install_pack(pack_id, packs_dir, check_exists, dry_run) Dict
close() None

FleetAlertSDK (fleet) key methods

All mutating methods require pool_uuid and node_id.

Method Returns Notes
list_alerts(pool_uuid, node_id, scope, search, status, risk, active, api_version) List[Dict] Does not paginate — always returns the complete list in one call
get_alert(pool_uuid, node_id, alert_id, scope) Dict
create_alert(pool_uuid, node_id, yaml_file, owner_id, ...) Dict Async order polled automatically
update_alert(pool_uuid, node_id, alert_id, yaml_file, ...) Dict
activate_alert(pool_uuid, node_id, alert_id) Dict
deactivate_alert(pool_uuid, node_id, alert_id) Dict
delete_alert(pool_uuid, node_id, alert_id) Dict
bulk_activate_alerts(pool_uuid, node_id, alert_keys) List[Dict] Per-key results; a key can be "indeterminate": True if its outcome couldn't be confirmed
bulk_deactivate_alerts(pool_uuid, node_id, alert_keys) List[Dict] Same per-key/"indeterminate" contract as bulk_activate_alerts
bulk_delete_alerts(pool_uuid, node_id, alert_keys) List[Dict] Same per-key/"indeterminate" contract
fetch_repos(pool_uuid, node_id, search_head_names) List[str]
resolve_mitre_attack_tags(pool_uuid, node_id, technique_ids) List[str] Falls back to parent technique for sub-techniques
refresh_token() str Returns new access token; updates session header
close() None

YAML Rule Validation

  • validate_yaml_rule(rule_data) in g6_alert_core.rules raises ValueError for missing required fields, invalid risk values, or wrong field types.
  • YAML loading is path-traversal-safe: symlinks and non-.yaml/.yml extensions are rejected with ValidationError before the file is opened. Containment is anchored per operation, not to the current working directory — the rule file's own directory for create/update, the pack's content-root for install-pack — so rule files can live in a Rules repo released and checked out separately from this SDK/CLI.

Rule YAML — minimum_guardsix_version

An optional top-level rule YAML field declaring the minimum Guardsix version the rule requires (e.g. minimum_guardsix_version: 7.6.0). Fleet only — Standalone has no endpoint to fetch its own version, so the field is inert there (no gating, no error). This supersedes the older minimum_logpoint_version metadata-only field, which is unrelated and unchanged.

  • create/update — blocks (raises) if the target node's actual version (from MachineInfo) doesn't meet the requirement, or can't be determined at all (fail closed). A rule with no minimum_guardsix_version never triggers the extra API call.
  • install-pack — skips just the affected (rule, target) pair with a visible skipped (requires Guardsix >= X (target is Y)) line; other rule/target pairs in the same run still proceed.
  • pack-diff — never blocks (read-only): a failing pair is flagged as a VERSION entry in the diff output alongside whatever other category (MISSING/CHANGED/…) applies.
  • Version comparison compares only the two versions' common prefix length — Guardsix now ships 4-component versions (e.g. 7.8.0.2) where it used to ship 3 (7.6.0), and rule YAML is still authored with 3 components, so a 4-part actual version is judged against a 3-part minimum using just its first 3 components.

Rule YAML — detection.timerange

The alert search window, e.g. timerange: 5 min. The Alert API splits this into three separate fields — timerange_day, timerange_hour, timerange_minute — of which exactly one may be non-zero; the SDK converts the single YAML string into the correct field on create/update for both Standalone and Fleet.

  • Format — strictly "<number> <unit>" with a single space and unit one of min, hr, or day (case-insensitive). Plurals (mins, hrs), long forms (minute, hour, hours), bare numbers (5), and glued forms (5min) are all rejected.
  • Range — min: 1–59, hr: 1–23, day: 1–30 (the server's own limits).
  • Default — a rule YAML with no timerange field defaults to 5 min.
  • Invalid or out-of-range values raise an error before any API call (from validate, create, and update), rather than surfacing a raw server rejection.
  • get/backup reconstruct detection.timerange from whichever of the three API fields is non-zero (e.g. timerange_hour: 2 → timerange: 2 hr).

Rule YAML — notification.syslog

Syslog notification configuration is Fleet-only at the moment. Standalone has no Syslog Notification API, so a notification.syslog block in the rule YAML is silently skipped there. After deploying rules to a Standalone SIEM, use the bulk syslog notification update feature in the SIEM UI to apply the required syslog notification template to the deployed rules.

On Fleet, the SDK builds the syslog message body for you. Your own field_mapping entries (the event columns you want in the message, e.g. incident_ts: log_ts) are rendered as the middle of the message. Around them, the SDK adds a fixed set of incident, rule, and threat-context fields automatically, so you don't need to list these in field_mapping yourself.

The incident and rule fields added before your mapped columns are incident_name, incident_risk_level, incident_log_source, alert_rule_author, alert_rule_reference, incident_object_id, and incident_id. The threat and MITRE ATT&CK fields added after your mapped columns are associated_malware, threat_actor, attack_category, attack_tag, attack_id, attack_framework, and incident_criteria. All of these are Fleet template tokens filled in from the incident and rule when the alert fires, except attack_framework, which is always emitted as the literal value MITRE.

This addition is unconditional: the fixed fields appear in the message even if field_mapping is empty. If you also list one of these names in your own field_mapping, it will appear twice in the message — once as your mapped entry and once as the field the SDK adds automatically.


discovery.json — Fleet Target Resolution

discovery.json is a Fleet-only file that maps pools and nodes to their UUIDs, hostnames, IPs, and topology roles. It is the primary source for target resolution when --pool is given without --node.

Generate

g6-alert --profile prod discover -o discovery.json

Queries monitorapi/v1/director/poolinfo for pool/node enumeration, then probes each node's DistributedLogpoints endpoint to assign roles, its SystemSettingsModesOfOperation/RawSyslogForwarder endpoints to flag the (mutually exclusive) Guardsix Collector / Raw Syslog Forwarder modes, and its MachineInfo endpoint for the running Guardsix version (guardsix_version). File permissions: 0o600.

Schema

The file is a multi-profile cache (version: 2), keyed by profile name. Each profile section contains its own fleet_url, last_updated timestamp, and discovery data. The discovery-cache version: 2 is intentionally distinct from the config file's version: 1.

{
  "version": 2,
  "profiles": {
    "prod": {
      "fleet_url": "https://fleet.example.com",
      "last_updated": "2026-06-08T10:00:00",
      "discovery": {
        "pools": [
          {
            "pool_uuid": "p1",
            "pool_name": "SOC-Pool",
            "nodes": [
              {
                "node_identifier": "lp-001",
                "hostname": "node-sh-01",
                "ip": "10.0.0.10",
                "status": "active",
                "is_search_head": true,
                "is_data_node": false,
                "is_collector": false,
                "is_raw_syslog_forwarder": false,
                "guardsix_version": "7.8.0.2",
                "private_ip": null
              },
              {
                "node_identifier": "lp-002",
                "hostname": "node-dn-01",
                "ip": "10.0.0.20",
                "is_search_head": false,
                "is_data_node": true,
                "is_collector": false,
                "is_raw_syslog_forwarder": false,
                "guardsix_version": "7.8.0.2",
                "private_ip": "10.0.0.20"
              }
            ]
          }
        ]
      }
    }
  }
}

Multiple profiles (prod, lab, …) coexist in the same file; g6-alert discover merges-updates only the active profile's section, leaving others untouched.

Consumption

  • The CLI resolves --pool SOC-Pool to all nodes in that pool.
  • install-pack and rule operations skip nodes where is_data_node, is_collector, or is_raw_syslog_forwarder is true — none of these can host alert rules or run analytics.
  • guardsix_version isn't itself used for target resolution — it's re-fetched live (via MachineInfo) at create/update/install-pack/pack-diff time for the minimum_guardsix_version rule-YAML gate (Fleet only; see Rule YAML — minimum_guardsix_version).
  • A warning is logged if a profile's last_updated is more than 7 days old; a missing file (or missing profile section) prompts g6-alert discover.
  • Default path: ~/.g6/discovery.json (co-located with the config file); overridden by --discovery-file or profile.discovery_file. Any explicit absolute path is accepted (not restricted to CWD or ~/.g6) — only a relative path containing a .. segment is rejected.

Distributed Guardsix Repo Resolution

How repo resolution behaves when a node is part of a Distributed Guardsix setup — a Search Head with one or more Data Nodes attached. Read alongside discovery.json.

Standalone

Standalone has no discovery file and no stored role model: it manages exactly one Guardsix instance, so is_search_head/is_data_node flags don't exist anywhere. Role detection is a runtime heuristic instead of a stored fact.

  • Search-Head detection. fetch_repos() calls the SIEM's own /Repo/get_all_searchable_logpoint. More than one row in the response (len(rows) > 1) means the instance is acting as a Search Head for that call.
  • Local-repo exclusion. When acting as a Search Head, any repo address starting with 127.0.0.1:5504/ (its own local copy) is dropped automatically, since a rule should never watch a Search Head's own local repo. Single-box installs with no Data Nodes (len(rows) == 1) keep local 127.0.0.1:5504/… addresses as normal — exclusion only applies once the instance is genuinely acting as a Search Head.
  • Bare-name resolution auto-fans-out. repo: Windows resolves against every fetched address ending in /Windows. If it exists on 2+ Data Nodes, all are included, never just one — no customer choice needed.
  • Local-address redirect. A hardcoded 127.0.0.1:5504/Windows (e.g. copied from another rule) is treated exactly like the bare name Windows and redirected to the real Data-Node address(es) when any exist.
  • Full remote address is a trusted override. A full host:port/name address that isn't the local prefix passes through unchanged, with no live lookup.
  • No --sh-target equivalent, and none needed. One instance means no "which Search Head should host this?" ambiguity; multi-Data-Node fan-out here means "include everything that matches" — the opposite of Fleet's ambiguity problem.

Caveats:

  • Resolution fails closed rather than guessing: an unreachable server (with a repo explicitly requested) or a name matching nothing in the fetched list raises an error asking for the full host:port/name address.
  • No repo configured means no built-in default. create/update, whether called directly or via install-pack's create-per-rule path, raise AlertSDKError when a rule has no repo from --repo or its own repo: field — there is no hardcoded fallback list. install-pack alone keeps one further, lowest-priority tier: a rule that pins no repo anywhere falls back to every auto-discovered repo (Search-Head-local copies excluded) rather than failing outright; it fails only when nothing is discoverable either.
  • The reported address is trusted verbatim, with no reachability check. If the SIEM reports a tunnel/private IP the SDK's host can't reach, resolution still "succeeds"; the write may fail downstream with no auto-correction.

Fleet

Fleet keeps an explicit, persisted role model instead of a runtime heuristic. g6-alert discover probes each node's DistributedLogpoints endpoint and writes is_search_head/is_data_node/cluster_id/search_head(s) into discovery.json, plus is_collector/is_raw_syslog_forwarder from the SystemSettingsModesOfOperation/RawSyslogForwarder endpoints. Every other command reads these instead of re-probing live, so re-run discover after any topology change — resolution acts on whatever discovery.json last recorded, not current server state.

  • Multi-Search-Head clusters are merged, not left ambiguous per pair. Search Heads sharing one or more Data-Node peers are grouped into a single cluster_id via union-find.
  • Non-analytics nodes never host alert rules, on every command. An explicit --node <id> naming a Data Node, Guardsix Collector, or Raw Syslog Forwarder fails loud immediately with the specific role in the error; a broad --pool fan-out excludes them silently but visibly (a skip — line). Guardsix Collector and Raw Syslog Forwarder are Distributed Guardsix components that forward to Data Nodes/Search Heads, so a given node may render as a cluster Data Node or as Standalone — only the mode is tracked, not that forwarding topology.
  • --sh-target is required only for the three commands that place a rule — create, update, install-pack. A targeted pool with 2+ Search Heads of the same cluster and no --sh-target fails closed with the candidate list. Every other command (list, get, activate/deactivate, delete, pack-diff, get-users, restore) has no such requirement: they fan out across every Search Head in the cluster and act wherever the rule actually exists (e.g. delete reports "not present" on the Search Heads without it, deletes it on the one that has it).
  • Repo auto-fetch excludes every Search Head's own local repos in the pool, the same principle as Standalone's local-repo exclusion, driven by discovery.json instead of a runtime heuristic. Auto-fetch resolves a named repo (bare name → full address); it's never used as a default when a rule has no repo at all.
  • Repo source priority, highest wins: CLI --repo, then the pack manifest's own per-rule repo: (install-pack only), then the rule YAML file's own top-level repo: (create/update skip the manifest-entry layer, since there's no manifest for a single rule). No repo from any of these sources is an error, not a default: create raises AlertSDKError outright; update falls back one further step to the target alert's own currently deployed repos, so a routine update that doesn't touch repo: doesn't wipe it, before raising if even that is empty. Neither ever substitutes a hardcoded repo list or silently expands to every repo in the pool.
  • pack-diff only flags repo as CHANGED when the pack explicitly pins one (manifest repo: or the rule YAML's own repo:) — a rule that never pins a repo is never flagged just because the auto-resolved repo set naturally drifts as Data Nodes come and go.

Caveats:

  • install-pack's own pre-resolution pass (before the create/update call) is non-strict as an optimization — an unmatched bare name there just passes through unchanged rather than raising. That's not the final word, though: the create/update SDK call underneath always re-validates via the same fail-closed resolver create/update use directly, so a genuinely unresolvable name still raises a clean client-side error (with the known-repos list), isolated to that one rule — it does not depend on how the server handles it.
  • A stale discovery.json can misclassify a Data Node as a normal target, or make an already-resolvable cluster look ambiguous again — always re-run discover after adding or moving a Data Node or Search Head.

Error Handling

All SDK errors are subclasses of AlertSDKError. Catching it covers every failure:

from g6_alert_core import AlertSDKError

try:
    results = client.create_alert("rules/my_detection.yaml", pools=["SOC-Pool"])
except AlertSDKError as e:
    print(f"SDK error: {e}")

For fine-grained handling, import the specific subclass:

from g6_alert_core import (
    AlertSDKError,
    AuthenticationError,
    AlertNotFoundError,
    AlertAlreadyActiveError,
    ValidationError,
    APIError,
)

try:
    client.activate(["64abc123def456"], pools=["SOC-Pool"])
except AlertNotFoundError:
    print("Alert ID not found — check the ID or scope")
except AlertAlreadyActiveError:
    print("Already active — nothing to do")
except AuthenticationError as e:
    print(f"Token problem: {e}")
except APIError as e:
    print(f"HTTP {e.status_code}: {e.response_text}")
except AlertSDKError as e:
    print(f"Unexpected SDK error: {e}")

Exception hierarchy

All inherit from AlertSDKError(Exception):

Exception Raised when
AuthenticationError Invalid credentials, expired/missing token, 401 after refresh attempt
ConfigurationError Missing or invalid configuration (URL, profile type, etc.)
ValidationError Bad YAML path (symlink, traversal, wrong extension), invalid pack manifest structure
AlertNotFoundError get_alert, activate, or deactivate — rule ID not found. delete on a missing ID succeeds as a no-op instead of raising.
AlertAlreadyActiveError Activating a rule that is already active (standalone)
AlertAlreadyInactiveError Deactivating a rule that is already inactive (standalone)
PoolNotFoundError Referenced pool not found (Fleet)
NodeNotFoundError Referenced node not found (Fleet)
PackManifestError Pack manifest missing, invalid schema, or security validation failure
APIError Non-2xx HTTP response, request failure after retries, Fleet order timeout/failure
AlertSDKError Base class; also raised directly for generic logic errors

APIError exposes .status_code, .response_text, and .response for programmatic inspection.

The CLI converts all AlertSDKError subclasses to a non-zero exit code and a ClickException message printed to stderr.


Architecture

g6-alert is built on a Dependency Inversion pattern: the AlertClient facade and CLI speak only to the Transport and Auth abstract base classes defined in g6-alert-core. Concrete backend implementations (StandaloneTransport, FleetTransport) are selected once, at startup in factory.py, and injected into the client. The unused backend's dependencies are loaded lazily, so only the active backend's packages are imported.

g6_alert_core                (no HTTP, no CLI)
  Transport ABC / Auth ABC
  AlertClient facade
  Exceptions / Security / Rules / Packs / Formatting

g6-alert                     (depends on core)
  factory.py       build_client(profile) → AlertClient
  cli.py           g6-alert commands
  backends/
    standalone/    AlertSDK + StandaloneTransport + StandaloneAuth
    fleet/         FleetAlertSDK + FleetTransport + FleetAuth

The core package's own module summary is on its PyPI page.


Development & Testing

# Install with dev extras (pytest, pytest-cov)
pip install -e "./g6-alert[dev]"

# Run the test suite
cd g6-alert
pytest -q

# Run with branch coverage
pytest --cov --cov-report=term-missing

# Also run the core package tests
cd ../g6_alert_core
pip install -e ".[dev]"
pytest -q

Tests use an in-memory FakeTransport — no HTTP, no Guardsix instance required. The tests/helpers.py module provides shared test doubles; tests/conftest.py provides auto-discovered cfg and patch_build fixtures.


Contributing

External contributions are closed for now. Both packages (g6-alert and g6-alert-core) are distributed through PyPI only at this stage.

For working on the code from source, see Development & Testing.


License

Copyright 2026 Guardsix A/S.

Licensed under the Apache License, Version 2.0. You may not use this software except in compliance with the License. See the LICENSE and NOTICE files for the full terms.

Changelog

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

This monorepo publishes two packages that version together:

  • g6-alert — CLI (g6-alert) and AlertClient facade
  • g6-alert-core — shared, HTTP-free domain layer

[1.0.2] - 2026-10-28

Fixed

  • Rule files, pack manifests and backups now load on Windows (g6-alert and g6-alert-core). Fleet create, update and restore, and install-pack and pack-diff on both backends, failed there with AttributeError: module 'os' has no attribute 'O_NOFOLLOW'.
  • No more false "readable by other users" warning on Windows (g6-alert). Windows has no POSIX mode bits, and the user profile's access control lists protect files under %USERPROFILE%.
  • Fleet config and auth.json files are now read as UTF-8 on every platform (g6-alert). They were already written as UTF-8.
  • Piped or redirected output no longer crashes on Windows (g6-alert). The CLI now writes it as UTF-8. When PowerShell pipes the output into another command, the CLI's symbols look garbled unless [Console]::OutputEncoding is UTF-8. A file redirect keeps them intact in PowerShell 7.4 and later. Windows PowerShell 5.1 garbles them in a redirected file too.
  • A mitre_map.csv that starts with a byte-order mark now loads (g6-alert-core). Excel's "CSV UTF-8" format adds one.
  • On Windows, saving a config, credential, discovery or backup file now retries for under a second if another process has the file open, for example antivirus or another g6-alert run (g6-alert).
  • If a refreshed Fleet token can't be saved, the CLI now prints a warning without -v (g6-alert). Fleet has already invalidated the old secret, so the next command may fail to authenticate.
  • A save that fails while setting the temp file's permissions no longer leaves the temp file behind on Windows (g6-alert).
  • A leading ~ now expands to the home directory in these file paths (g6-alert and g6-alert-core): --config and G6_ALERT_CONFIG, --discovery-file and discover -o, --backup-file, G6_MITRE_MAP_FILE, the rule file given to create and update, and the auth_file, discovery_file and mitre_map_file profile values. This matters wherever the shell doesn't expand ~, such as config files, quoted values and Windows shells.
  • restore --backup-file now prints a short error instead of a Python traceback when the file is missing, is a directory or a symlink, or isn't valid YAML (g6-alert).
  • YAML errors in rule files, pack manifests and backups now give the file's path in their location lines, instead of a number (g6-alert-core).

[1.0.1] - 2026-10-27

Security

  • Raised the minimum PyJWT version to 2.15.0 (g6-alert). This clears CVE-2026-102265, -102266, -102267, -102268, -102269, -102270, -102271, -102272, -102273, -102274, -102275, CVE-2026-101917, -101918 and CVE-2026-103001. The SDK only signs tokens with jwt.encode and never decodes or verifies them, so none of these are reachable from SDK code.
  • Raised the minimum urllib3 version to 2.8.0 (g6-alert). This clears CVE-2026-97687, -97688 and -97689. A malicious server could use two of them to exhaust memory or hang the client.
  • Raised the minimum werkzeug version to 3.1.9 (g6-alert and g6-alert-core). This clears CVE-2026-102598. The flaw is in safe_join on Windows, which the SDK does not use.
  • Raised the minimum certifi version to 2026.10.19 (g6-alert). This clears GHSA-6q48-6c7w-9gw2, which has no CVE. Older CA bundles trust the emSign ECC Root CA - G3 root, and keys for several of its intermediate CAs were compromised. An attacker with one of those keys and a position on the network could impersonate a Guardsix server. certifi is now a direct dependency, because requests alone still allows the old versions.
  • Upgrade with pip install -U g6-alert. On 1.0.0, pip install -U PyJWT urllib3 werkzeug certifi picks up the same fixes.

[1.0.0] - 2026-09-03

Initial public release.

  • AlertClient facade and g6-alert CLI working against either a Standalone Guardsix instance or a Fleet console, selected by a typed profile.
  • Profile-based configuration (~/.g6/config.yaml): create, list, show, switch, and delete profiles.
  • Alert rule lifecycle: create (from YAML), update, list, show, activate, and deactivate, for both Standalone and Fleet backends.
  • Fleet pool/node discovery (discovery.json) and detection-pack installation (install-pack).
  • MITRE ATT&CK attack-tag resolution for Standalone rules via a bundled/local technique map.
  • Authentication support for Standalone (local self-signed JWT) and Fleet (remote Bearer token with refresh), with an auth.json credential file format shared across backends.
  • TLS certificate verification enabled by default; override per run with --tls-verify/--no-tls-verify, per profile with verify_ssl:, or via G6_VERIFY_SSL/G6_FLEET_VERIFY_SSL for self-signed hosts.
  • Path-security YAML loading, rule/pack-manifest validation, and consistent table/JSON output formatting shared via g6_alert_core.
  • Apache-2.0 licensing.

Metadata

Release files for g6-alert 1.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for g6-alert 1.0.2
File Size Uploaded Provenance Actions
g6_alert-1.0.2.tar.gz 311.6 kB Without provenance Details for g6_alert-1.0.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for g6-alert 1.0.2
File Interpreter ABI Platform Actions
g6_alert-1.0.2-py3-none-any.whl Python 3 none any Details for g6_alert-1.0.2-py3-none-any.whl

Total release size: 503.0 kB

Release files / g6_alert-1.0.2.tar.gz

Download URL g6_alert-1.0.2.tar.gz
Size 311.6 kB
Tags Source
SHA-256 checksum
How to use checksums
26072120be5c322d6486040b40f02f2fa0610fc759df2e03848d288dc464503f
BLAKE2b-256 checksum
How to use checksums
3a3ba79296a9e39e43623e44cfe6d34428db3933c6b8071828512a76da594743
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.18

Release files / g6_alert-1.0.2-py3-none-any.whl

Download URL g6_alert-1.0.2-py3-none-any.whl
Size 191.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
aba39fa1043eae81ffb0f83cc3c34c7a10f1daad0eac04d272dc3b1f39e2a53c
BLAKE2b-256 checksum
How to use checksums
580723646d9937cb1f82f8c4424fbdd1ce61a9fbb0bf97e7e51419d5ef7bda77
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.18

Security for g6-alert 1.0.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page