g6-alert — Unified Guardsix Alert-Rule SDK & CLI
One CLI and one
AlertClientfacade for managing Guardsix alert (detection) rules across Standalone instances and Fleet consoles, selected by a typed profile.
Table of Contents
- Overview
- Features
- Standalone vs. Fleet: what differs
- Requirements
- Installation
- Quick Start
- Configuration
- Authentication
- auth.json — Credential File Format
- CLI Usage
- Programmatic Usage
- API Reference
- YAML Rule Validation
- discovery.json — Fleet Target Resolution
- Distributed Guardsix Repo Resolution
- Error Handling
- Architecture
- Development & Testing
- Contributing
- License
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-alertdrives both Standalone and Fleet with the same verbs. Fleet-only commands are hidden from--helpunder a standalone profile. - Unified
AlertClientfacade 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-packapplies an entire manifest of rules in one command (--dry-run,--validate-only,--update-if-exists, and Standalone-only--atomicrollback-on-failure). An existing rule found deactivated is reinstalled and reactivated automatically.--prune/--prune-deletedeactivate, or (deprecated rules only, backed up) permanently delete, rules a pack no longer wants live. pack-diffpreviews a pack against one owner's deployment: missing, changed (including drifted active/inactive state), orphaned, deprecated-still-deployed, and ambiguous rules — the same computationinstall-pack/--pruneact on.- Fleet topology awareness.
discoveridentifies 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 byinstall-packand rule operations. A Distributed Guardsix cluster (2+ Search Heads sharing the same Data Nodes) needs--sh-targetto 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 optionallyupdate) snapshots each rule before modification;restorerecreates a rule from any snapshot. Standalone has no backup mechanism. - Path-traversal-safe YAML loading —
O_NOFOLLOW+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 /refreshTokenon a 401 when asecret_keyis 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-alertandg6-alert-coredeclarerequires-python = ">=3.11"; CI tests 3.11, 3.12, and 3.13. - Runtime dependencies (installed automatically):
requests,urllib3,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.0).
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:
--profile NAMECLI flagG6_PROFILEenvironment variabledefault_profilein 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(defaultHS256). 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_URLBase URL G6_SUBJECTSubject (username) G6_SECRETSigning secret G6_VERIFY_SSLTLS verification G6_MITRE_MAP_FILEPath to the MITRE technique->hash mapping CSV G6_VERIFY_SSLacceptstrue,on,yes, or1(case-insensitive) to enable verification; anything else disables it. It overrides the profile'sverify_sslvalue. -
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:
--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.~/.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.- Otherwise, the CSV bundled with the package.
- Resolution mirrors Fleet's own sub-technique fallback: an exact
attack_idmatch 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 — thecreate/updatestill 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}/refreshTokenwith body{"secretKey": "<secret_key>"}. Returns{"tokenDetails": {"token": ..., "secretKey": ...}}. A 401 with nosecret_keypresent surfaces immediately to the caller. -
Token age lifecycle (surfaced by
token-check):Age Status < 4 h ok4 – 8 h monitor≥ 8 h expired— renew withrefresh-token(also auto-refreshes on a 401) -
Environment variables:
Variable Field G6_FLEET_URLFleet console URL G6_FLEET_ACCESS_TOKENBearer token G6_FLEET_SECRET_KEYRefresh secret G6_FLEET_VERIFY_SSLTLS verification G6_VERIFY_SSLTLS verification (checked if G6_FLEET_VERIFY_SSLis 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_iddiverges (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 confirmedrule_idmatch. A name-only match (norule_idtag 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-existsis passed. Repo is only compared when the pack pins one explicitly (the manifest rule entry's ownrepo:, or failing that the rule YAML file's own top-levelrepo:); 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: deprecatedand it's still deployed under this owner (scheduled for removal, not yet cleaned up).install-pack --prune/--prune-deleteonly acts on a confirmedrule_idmatch; a name-only match is held back and reported asNEEDS REVIEWinstead, 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_versionand 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, sincepack-diffnever 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
--helpunder 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)ing6_alert_core.rulesraisesValueErrorfor missing required fields, invalidriskvalues, or wrong field types.- YAML loading is path-traversal-safe: symlinks and non-
.yaml/.ymlextensions are rejected withValidationErrorbefore the file is opened. Containment is anchored per operation, not to the current working directory — the rule file's own directory forcreate/update, the pack's content-root forinstall-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 (fromMachineInfo) doesn't meet the requirement, or can't be determined at all (fail closed). A rule with nominimum_guardsix_versionnever triggers the extra API call.install-pack— skips just the affected (rule, target) pair with a visibleskipped (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 aVERSIONentry 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 andunitone ofmin,hr, orday(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
timerangefield defaults to5 min. - Invalid or out-of-range values raise an error before any API call (from
validate,create, andupdate), rather than surfacing a raw server rejection. get/backupreconstructdetection.timerangefrom 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-Poolto all nodes in that pool. install-packand rule operations skip nodes whereis_data_node,is_collector, oris_raw_syslog_forwarderistrue— none of these can host alert rules or run analytics.guardsix_versionisn't itself used for target resolution — it's re-fetched live (viaMachineInfo) at create/update/install-pack/pack-diff time for theminimum_guardsix_versionrule-YAML gate (Fleet only; see Rule YAML —minimum_guardsix_version).- A warning is logged if a profile's
last_updatedis more than 7 days old; a missing file (or missing profile section) promptsg6-alert discover. - Default path:
~/.g6/discovery.json(co-located with the config file); overridden by--discovery-fileorprofile.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 local127.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: Windowsresolves 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 nameWindowsand redirected to the real Data-Node address(es) when any exist. - Full remote address is a trusted override. A full
host:port/nameaddress that isn't the local prefix passes through unchanged, with no live lookup. - No
--sh-targetequivalent, 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/nameaddress. - No repo configured means no built-in default.
create/update, whether called directly or viainstall-pack's create-per-rule path, raiseAlertSDKErrorwhen a rule has no repo from--repoor its ownrepo:field — there is no hardcoded fallback list.install-packalone 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_idvia 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--poolfan-out excludes them silently but visibly (askip —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-targetis 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-targetfails 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.deletereports "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.jsoninstead 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-rulerepo:(install-packonly), then the rule YAML file's own top-levelrepo:(create/updateskip 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:createraisesAlertSDKErroroutright;updatefalls back one further step to the target alert's own currently deployed repos, so a routine update that doesn't touchrepo: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-diffonly flags repo as CHANGED when the pack explicitly pins one (manifestrepo:or the rule YAML's ownrepo:) — 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: thecreate/updateSDK call underneath always re-validates via the same fail-closed resolvercreate/updateuse 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.jsoncan misclassify a Data Node as a normal target, or make an already-resolvable cluster look ambiguous again — always re-rundiscoverafter 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.
Metadata
Release files for g6-alert 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| g6_alert-1.0.0.tar.gz | 300.5 kB | Without provenance | Details for g6_alert-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| g6_alert-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for g6_alert-1.0.0-py3-none-any.whl |
Total release size: 487.7 kB
Release files / g6_alert-1.0.0.tar.gz
| Download URL | g6_alert-1.0.0.tar.gz |
|---|---|
| Size | 300.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4c8f7914981ec10dae0d5102d71fa589774018fada49b0c9aac39ad9b9b2a88c
|
|
BLAKE2b-256 checksum How to use checksums |
041dfd507519f6c732e1608b0784c93ddded8bd1dcc5b6c7b0b8388c1ba1168a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.16
|
Release files / g6_alert-1.0.0-py3-none-any.whl
| Download URL | g6_alert-1.0.0-py3-none-any.whl |
|---|---|
| Size | 187.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9c19354a9049cf563a4701abc1e9371605862ebf68f4d4aeb9bc847cd8b82a94
|
|
BLAKE2b-256 checksum How to use checksums |
e7d9b8c5e609a71797ed6056a9465f4bee29160aba898b12257bbae4dd5e816c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.16
|
Security for g6-alert 1.0.0
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.