Network Security & Compliance Auditor — automated SSH baseline checks to detect and prevent configuration drift
Project description
Network Security & Compliance Auditor
┌─────────────────────────────────────────────────────────────────┐
│ AUDNET ARCHITECTURE │
│
│ ┌──────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ YAML │───▶│ Collector │───▶│ TextFSM Parser │ │
│ │ Inventory│ │ (Netmiko + │ │ (CLI → JSON) │ │
│ │ +Baseline│ │ ThreadPool)│ └────────┬───────────┘ │
│ └──────────┘ └──────┬───────┘ │ │
│ │ ▼ │
│ │ ┌────────────────────┐ │
│ │ │ Compliance Engine │ │
│ │ │ (4 Security Rules) │ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌────────────────────┐ │
│ │ DeviceSnapshot │──▶│ Report Generator │ │
│ │ (Pydantic) │ │ (Jinja2 → MD/HTML) │ │
│ └──────────────────┘ └────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ audit_report.md │ │
│ │ audit_report.html │ │
│ └────────────────────┘ │
│ │
│ Parallel SSH ──▶ 4 devices concurrently (configurable) │
│ All layers independently testable — no real hardware needed │
└─────────────────────────────────────────────────────────────────┘
Problem Statement
In production networks, configuration drift is inevitable. Engineers make manual changes that bypass security baselines — enabling SSHv1, leaving switchports on default VLANs, or pointing NTP/syslog to unauthorized servers. Traditional auditing is manual, error-prone, and doesn't scale.
audnet solves this by automating SSH-based compliance audits against security baselines. This detects drift in real-time and prevents future drift by enforcing hardened policies.
Solution
A Python CLI tool that:
- Connects in parallel to multiple routers/switches via SSH (Netmiko + ThreadPool, with retries)
- Pulls live state —
show ip interface brief,show version,show running-config - Parses unstructured CLI into clean JSON using TextFSM templates
- Audits against baselines — flags SSHv1, unauthorized VLANs, rogue NTP/syslog servers
- Generates reports — professional Markdown and HTML with pass/fail summaries
- Supports filters & JSON for targeted runs and CI integration
Every layer is independently testable with mocked responses — no real network hardware required.
Installation
Quick install (end users)
# From PyPI (recommended for production)
pip install audnet
# Or with uv
uv tool install audnet
# From source (latest development version)
pip install git+https://github.com/islam666/Audnet.git
# Verify
audnet --version
Development setup (contributors)
Prerequisites
- Python 3.12+
- uv package manager
- Linux/macOS environment
Step-by-step setup
# 1. Clone the repository
git clone https://github.com/islam666/Audnet.git
cd audnet
# 2. Install dependencies (uses uv.lock for reproducible installs)
uv venv
uv pip install -e ".[dev]"
# 3. Activate virtual environment
source .venv/bin/activate
# 4. Install pre-commit hooks
pre-commit install
# 5. Verify installation
python -c "import audnet; print(audnet.__version__)"
# Expected: 0.1.1
# 6. Run the test suite
pytest tests/ -v
# Expected: 200+ passed
uv pip install -e ".[dev]" reads the committed uv.lock to install the exact same dependency versions across all environments. Use uv lock (no args) to regenerate the lockfile after adding new dependencies.
Quick start
# Dry run against the sample inventory — no SSH connections made
audnet audit --dry-run
# Run a real audit against your devices
audnet audit --inventory inventories/devices.yaml --baseline baselines/security_baseline.yaml
Configure device inventory
Edit inventories/devices.yaml with your network devices:
defaults:
device_type: cisco_ios
port: 22
devices:
- name: core-router-01
host: 192.168.1.1
username: admin
password: "${AUDNET_PASSWORD}" # resolved from environment
Set the password via environment variable:
export AUDNET_PASSWORD="your-secret-password"
SSH key-based authentication
Instead of password authentication, use SSH keys:
devices:
- name: core-router-01
host: 192.168.1.1
username: admin
use_keys: true
key_file: ~/.ssh/id_ed25519
use_keys: true— enable SSH key authenticationkey_file— path to the private key file (optional; uses SSH agent or default keys if omitted)
Customize security baseline
Edit baselines/security_baseline.yaml to match your organization's policies:
checks:
ssh_version:
severity: critical
rule: ssh_v2_only
inactive_ports:
severity: high
rule: no_open_ports
allowed_vlans: [10, 20, 30] # your secure VLANs
ntp_config:
severity: medium
rule: ntp_approved
approved_servers:
- 10.0.0.50
syslog_config:
severity: medium
rule: syslog_approved
approved_servers:
- 10.0.0.60
Usage
Run a full audit
source .venv/bin/activate
audnet audit \
--inventory inventories/devices.yaml \
--baseline baselines/security_baseline.yaml \
--output audit_report \
--format both \
--workers 4
Advanced usage (new in this release)
Filter to one device or specific checks, output JSON for scripting:
audnet audit --device core-router-01 --check ssh_v2_only,ntp_config --json
Async mode (recommended for >20 devices)
The asyncio-based collector uses asyncssh for lower memory overhead and better
scalability. It is recommended for audits involving more than 20 devices.
audnet audit --async
Trade-offs vs the default sync collector:
| Sync (default) | Async (--async) |
|
|---|---|---|
| Dependency | Netmiko | asyncssh |
| Concurrency | ThreadPool | asyncio Semaphore |
| Best for | <20 devices | >20 devices |
| Memory per connection | Higher (thread stack) | Lower (coroutine) |
Usage Examples
All examples below use the default inventory and baseline paths. Adjust as needed.
Audit a single device
audnet audit --device core-router-01
Run specific checks only
Using comma-separated values in a single --check:
audnet audit --check ssh_v2_only,ntp_config
Or repeat the flag:
audnet audit --check ssh_v2_only --check ntp_config
JSON output for CI/CD pipelines
audnet audit --json
Example output:
[
{
"device_name": "core-router-01",
"overall_pass": true,
"checks": [
{"check_name": "ssh_v2_only", "passed": true, "detail": "SSHv2 configured"},
{"check_name": "ntp_approved", "passed": false, "detail": "unauthorized NTP: 10.0.0.99"}
]
}
]
Pipe to jq for targeted queries:
audnet audit --json | jq '.[] | select(.overall_pass == false) | .device_name'
Dry-run mode
Validate your config without touching devices:
audnet audit --dry-run
Combine with filters to preview a targeted run:
audnet audit --dry-run --device core-router-01 --check ssh_v2_only
Strict mode for CI
Fail immediately if any device has a plaintext password (no ${ENV_VAR} reference).
Checks password, secret, passwd, and token fields:
audnet audit --strict
Without --strict, a warning is logged instead of failing.
Verbose debug logging
audnet audit -v --dry-run
Combined: single device, specific check, JSON, strict
audnet audit --device core-router-01 --check ssh_v2_only --json --strict
Allow compliance failures without non-zero exit
By default, audnet exits with code 1 when compliance checks fail. Use --no-fail
to always exit with code 0 (useful when you want the report but don't want CI to break):
audnet audit --no-fail
Show version
audnet --version
Sample Output
$ audnet audit --inventory inventories/devices.yaml
[INFO] Loaded 2 devices from inventory
[INFO] Connecting in parallel (workers=4)...
core-router-01: ✓ passed (4/4 checks)
dist-switch-02: ✗ failed (SSHv1 enabled, Gi0/3 on unauthorized VLAN 1)
Report: audit_report.md + audit_report.html generated.
Summary: 1 passed, 1 with issues.
CLI options
| Option | Default | Description |
|---|---|---|
--inventory |
inventories/devices.yaml |
Device inventory YAML path |
--baseline |
baselines/security_baseline.yaml |
Security baseline YAML path |
--output |
audit_report |
Output file prefix |
--format |
both |
Output format: md, html, or both |
--workers |
4 |
Max parallel SSH connections |
--device |
(all) | Filter to single device by name |
--check |
(all) | Filter to specific checks (repeatable; comma-separated) |
--json |
false |
Output JSON summary to stdout |
--dry-run, -n |
false |
Validate config without connecting to devices |
--strict |
false |
|
--no-fail |
false |
|
-v, --verbose |
false |
|
--version |
— | |
--async |
false |
|
--connect-timeout |
10 |
Dry-run mode
Use --dry-run (or -n) to validate your inventory and baseline and preview what would be audited — no SSH connections made:
audnet audit --inventory inventories/devices.yaml --dry-run
Output:
audnet v0.1.0 — Starting audit...
Loaded 2 devices, 4 checks
DRY RUN — no device connections will be made
Devices that would be audited:
• core-router-01 (192.168.1.1) — cisco_ios
• dist-switch-02 (192.168.1.2) — cisco_ios
Checks that would be run:
• inactive_ports
• ntp_config
• ssh_v2_only
• syslog_config
Dry run complete — config and baseline are valid
Combine with --device and --check to filter the preview:
audnet audit --dry-run --device core-router-01 --check ssh_v2_only
Output
The tool produces:
- Terminal summary — Rich table with per-device pass/fail status
- audit_report.md — Markdown report with detailed findings table
- audit_report.html — Styled HTML report for sharing
- JSON (with --json) — Machine-readable for CI/CD
Project Structure
audnet/
├── pyproject.toml # Build config, dependencies, pytest/ruff settings
├── CHANGELOG.md # Release history (Keep a Changelog format)
├── CONTRIBUTING.md # Development guidelines, testing, PR workflow
├── LICENSE # MIT License
├── README.md # This file
├── SECURITY.md # Security policy, credential handling, disclosure
├── uv.lock # Reproducible dependency lockfile
├── .pre-commit-config.yaml # Pre-commit hooks (ruff, mypy, bandit, etc.)
├── benchmarks/
│ └── bench_collectors.py # Sync vs async collector performance benchmarks
├── inventories/
│ └── devices.yaml # Sample device inventory
├── baselines/
│ └── security_baseline.yaml # Compliance rules configuration
├── src/audnet/
│ ├── __init__.py # Package init, version
│ ├── cli.py # Typer CLI entry point
│ ├── config.py # YAML inventory/baseline loader with env resolution
│ ├── models.py # Pydantic data models (incl. SecurityBaseline)
│ ├── exceptions.py # Structured exception hierarchy
│ ├── vendor_registry.py # Vendor registry for multi-vendor dispatch
│ ├── collector.py # Parallel SSH collector (Netmiko + ThreadPool + retries)
│ ├── collector_async.py # Asyncio collector (asyncssh + semaphore concurrency)
│ ├── parser.py # TextFSM parser (CLI → structured JSON, vendor-aware)
│ ├── compliance.py # Rule engine (4 security checks, vendor-pattern overrides)
│ ├── reporter.py # Jinja2 report generator (Markdown + HTML)
│ ├── templates/
│ │ ├── __init__.py
│ │ ├── audit_report.md.j2 # Markdown report template
│ │ └── audit_report.html.j2 # HTML report template
│ └── textfsm_templates/
│ ├── __init__.py
│ ├── cisco_ios_show_ip_interface_brief.textfsm
│ ├── cisco_ios_show_version.textfsm
│ ├── cisco_ios_show_running_config.textfsm
│ ├── cisco_ios_show_interface_status.textfsm
│ └── cisco_ios_show_cdp_neighbors_detail.textfsm
└── tests/
├── __init__.py
├── conftest.py # Shared pytest fixtures
├── test_models.py # Device, ComplianceResult, AuditReport
├── test_config.py # Inventory loading, env resolution
├── test_collector.py # SSH collection, error handling, vendor dispatch
├── test_collector_async.py # Async collector: success, auth failure, timeout, mixed
├── test_parser.py # TextFSM parsing, vendor-aware template selection
├── test_compliance.py # All 4 rule types (pass/fail), case-insensitive
├── test_reporter.py # Markdown/HTML rendering
├── test_vendor_registry.py # Vendor profiles, dispatch, registration
├── test_exceptions.py # Exception hierarchy and inheritance
├── test_integration.py # End-to-end: compliant, noncompliant, partial
├── test_logging.py # Structlog configuration and secret redaction
└── test_version.py # Version string format and accessibility
Multi-Vendor Support
audnet uses a vendor registry/dispatch pattern (similar to NAPALM/Nornir driver architecture) for multi-vendor support. Device types are resolved automatically, with Cisco IOS as the fallback default.
Supported vendors
| Vendor | device_type | Template prefix |
|---|---|---|
| Cisco IOS/IOS-XE | cisco_ios |
cisco_ios |
| Cisco NX-OS | cisco_nxos |
cisco_nxos |
| Arista EOS | arista_eos |
arista_eos |
Unknown device types fall back to cisco_ios commands and templates.
Configuring devices for different vendors
Set device_type per-device or as a default in your inventory YAML:
defaults:
device_type: cisco_ios
devices:
- name: core-router-01
host: 192.168.1.1
username: admin
password: "${AUDNET_PASSWORD}"
- name: nexus-switch-01
host: 192.168.1.2
device_type: cisco_nxos
username: admin
password: "${AUDNET_PASSWORD}"
- name: arista-leaf-01
host: 192.168.1.3
device_type: arista_eos
username: admin
password: "${AUDNET_PASSWORD}"
Adding a new vendor
Adding support for a new network OS takes three steps. No changes to parser, collector, or compliance code are needed — the vendor registry pattern handles dispatch automatically.
Step 1: Add TextFSM templates
Create one template per data slot in textfsm_templates/. The naming convention is <prefix>_<slot_suffix>.textfsm, where the suffix matches the slot names used by the built-in vendors:
| Slot | Purpose | Example suffix |
|---|---|---|
show_ip_interface_brief |
Interface status | show_ip_interface_brief |
show_version |
Device version/info | show_version |
show_running_config |
Full running config | show_running_config |
For example, to add Juniper JunOS:
textfsm_templates/
├── juniper_junos_show_ip_interface_brief.textfsm
├── juniper_junos_show_version.textfsm
└── juniper_junos_show_running_config.textfsm
Each template should parse the vendor's equivalent CLI output into the same column names the compliance engine expects (e.g., INTERFACE, IP_ADDRESS, STATUS, PROTOCOL for interfaces).
Tip: Use the TextFSM CLI tool to interactively test templates against sample output before committing.
Step 2: Register the vendor
You have two options — static registration (recommended for built-in vendors) or runtime registration (for plugins or dynamic use).
Option A: Static registration — add to VENDOR_PROFILES in src/audnet/vendor_registry.py:
VENDOR_PROFILES["juniper_junos"] = _profile(
commands=[
"show interfaces terse",
"show version",
"show configuration",
],
prefix="juniper_junos",
description="Juniper JunOS",
)
The commands list must have exactly three entries matching the three slots above (interface brief, version, running config). The prefix must match the TextFSM template filename prefix.
Option B: Runtime registration — call register_vendor() from your code or a plugin:
from audnet.vendor_registry import register_vendor
register_vendor(
device_type="juniper_junos",
commands=["show interfaces terse", "show version", "show configuration"],
template_prefix="juniper_junos",
)
Runtime registration is useful for plugins, tests, or adding vendors without modifying the audnet source.
Step 3: (Optional) Add vendor-specific compliance patterns
If the vendor uses different CLI syntax for the same security concepts, add vendor_patterns to your baseline YAML:
checks:
ssh_version:
severity: critical
rule: ssh_v2_only
vendor_patterns:
juniper_junos:
match: "set system ssh"
ok_value: "set system ssh protocol-v2"
The key under vendor_patterns must match the device_type used in the inventory. If no vendor-specific pattern is defined, the default pattern is used.
Step 4: Configure devices in inventory
Set the device_type on your devices to match the registered key:
devices:
- name: juniper-router-01
host: 192.168.1.10
device_type: juniper_junos
username: admin
password: "${AUDNET_PASSWORD}"
That's it. The collector will automatically send the correct commands, the parser will load the correct templates, and the compliance engine will use the correct patterns.
Verifying your vendor
Run a dry-run to confirm the vendor is recognized:
audnet audit --device juniper-router-01 --dry-run
Then run a full audit and check the output:
audnet audit --device juniper-router-01 --json
How it works
vendor_registry.pymapsdevice_typeto CLI commands and TextFSM template prefixescollector.pycallsget_commands(device_type)instead of a hardcoded dictparser.pycallsget_template_name(device_type, slot)for dynamic template loadingcompliance.pyuses pattern-based matching with optional per-vendor overrides- All vendor resolution falls back to
cisco_iosfor unknown device types
Performance & Scalability
Current architecture: ThreadPool + Netmiko
The default collector (collector.py) uses concurrent.futures.ThreadPoolExecutor
with Netmiko for SSH. This works well for small-to-medium inventories (up to
~20 devices) but has limitations at scale:
- Thread overhead: Each concurrent connection consumes a thread (~8MB stack)
- GIL contention: Python's GIL limits true parallelism for CPU-bound parsing
- Memory: 100 devices × 4 threads = significant memory for thread stacks
Async prototype: asyncio + asyncssh
An async collector prototype is available at collector_async.py. It replaces
threads with coroutines and uses asyncssh for SSH transport:
| Aspect | Sync (ThreadPool) | Async (asyncio) |
|---|---|---|
| Concurrency model | OS threads | Coroutines |
| Memory per connection | ~8MB (thread stack) | ~1KB (coroutine) |
Default max_workers |
4 | 50 |
| Scales to | ~20-50 devices | 100+ devices |
| Dependency | Netmiko | asyncssh |
Running the benchmark
uv run python benchmarks/bench_collectors.py
This compares sync vs async collection across 4/8/16/32 devices with mocked
SSH responses. Results are written to benchmarks/results.json.
Migration path
The async collector is a prototype — it produces identical DeviceSnapshot
output and shares the same parser, compliance, and vendor registry code.
To switch to async collection when scaling beyond ~50 devices:
- Install asyncssh:
uv add asyncssh - Change the import in
cli.py:# from audnet.collector import collect_all from audnet.collector_async import collect_all_async as collect_all
- The
--workersflag maps toasyncio.Semaphorelimit (default: 50) - Keep the sync collector as fallback for environments without asyncssh
Future: Scrapli
For production async deployments, consider migrating from asyncssh to
Scrapli which provides:
- Built-in multi-vendor support (replacing Netmiko's device-type abstraction)
- Both sync and async transports
- Structured parsing (replacing TextFSM for some platforms)
- Active community and regular updates
The vendor registry pattern in vendor_registry.py is already compatible —
Scrapli would replace only the SSH transport layer in the collector.
Compliance Checks
| Check | Rule | Severity | What it detects |
|---|---|---|---|
| SSH Version | ssh_v2_only |
Critical | SSHv1 enabled or SSHv2 not configured |
| Inactive Ports | no_open_ports |
High | Switchports in unauthorized VLANs |
| NTP Config | ntp_approved |
Medium | NTP servers not in approved list |
| Syslog Config | syslog_approved |
Medium | Syslog servers not in approved list |
Adding a new compliance rule
- Write a
_check_your_rule(snapshot, config) -> ComplianceResultfunction incompliance.py - Add it to the
_RULE_DISPATCHdict - Add the rule config to
baselines/security_baseline.yaml - Write tests in
test_compliance.py
Contributing
See CONTRIBUTING.md for development setup, adding rules, testing, and PR workflow.
Testing
# Run all tests
pytest tests/ -v
# Run with coverage
pytest tests/ --cov=audnet --cov-report=term-missing
# Run specific test file
pytest tests/test_compliance.py -v
# Lint
ruff check src/ tests/
All tests use mocked device responses — no real SSH connections or network hardware needed.
Security
audnet takes credential handling seriously. Passwords are stored as SecretStr (Pydantic) and are never rendered in logs or output.
Quick Start: Environment Variables
Use ${ENV_VAR} placeholders in inventory files:
devices:
- name: core-switch-01
host: 10.0.0.1
password: "${AUDNET_PASSWORD}"
export AUDNET_PASSWORD="your-secret"
audnet audit
Production: External Secret Stores
For production, use a dedicated secret manager instead of environment variables:
| Store | Example |
|---|---|
| HashiCorp Vault | export AUDNET_PASSWORD=$(vault kv get ...) |
| AWS Secrets Mgr | export AUDNET_PASSWORD=$(aws secretsmanager ...) |
| 1Password CLI | export AUDNET_PASSWORD=$(op read ...) |
| Python keyring | keyring.set_password("audnet", ...) |
See SECURITY.md for detailed integration examples.
Strict Mode (CI/CD)
Use --strict in CI pipelines to enforce that no plaintext passwords exist in inventory files:
audnet audit --strict
This fails with a ConfigError if any device has a password that is not a ${ENV_VAR} reference. Without --strict, a warning is logged instead.
SSH Key Authentication
Prefer SSH keys over passwords:
devices:
- name: core-switch-01
host: 10.0.0.1
use_keys: true
key_file: ~/.ssh/id_ed25519
Checklist
- Never commit inventory files with plaintext passwords
- Add
inventories/*.yamlto.gitignore(commit onlyinventories/example.yaml) - Use
.envfor local development (add.envto.gitignore) - Use
--strictin CI/CD - Prefer SSH key authentication
- Rotate credentials regularly
See SECURITY.md for the full security policy, vulnerability reporting, and responsible disclosure.
Changelog
See CHANGELOG.md for a detailed history of changes, new features, and bug fixes.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file audnet-0.1.2.tar.gz.
File metadata
- Download URL: audnet-0.1.2.tar.gz
- Upload date:
- Size: 138.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d92e9d6bc27be26dbf7432fc898a7802671265968537b5762bb0dde5316aed82
|
|
| MD5 |
380655b9142d667b084beec12f0e1141
|
|
| BLAKE2b-256 |
3cbc5d65bc2e8ce57dc216d3a1ba3d8f442f76e8fbac2df8914120eeb3372e3d
|
Provenance
The following attestation bundles were made for audnet-0.1.2.tar.gz:
Publisher:
publish.yml on islam666/Audnet
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
audnet-0.1.2.tar.gz -
Subject digest:
d92e9d6bc27be26dbf7432fc898a7802671265968537b5762bb0dde5316aed82 - Sigstore transparency entry: 1804271065
- Sigstore integration time:
-
Permalink:
islam666/Audnet@67e8d25e8f182d3e01d440134997802e4741563a -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/islam666
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@67e8d25e8f182d3e01d440134997802e4741563a -
Trigger Event:
push
-
Statement type:
File details
Details for the file audnet-0.1.2-py3-none-any.whl.
File metadata
- Download URL: audnet-0.1.2-py3-none-any.whl
- Upload date:
- Size: 30.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
684e3de09f573250f27e8bc148db00f93ba7a14d923e9cec05939051f39ac1ec
|
|
| MD5 |
bce0c2249eec5d8c5fab1dff8f006e19
|
|
| BLAKE2b-256 |
7948eccb4f54fb5f010080bac3065924b2bad5ef7ce5bd898da65f4c514a53a2
|
Provenance
The following attestation bundles were made for audnet-0.1.2-py3-none-any.whl:
Publisher:
publish.yml on islam666/Audnet
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
audnet-0.1.2-py3-none-any.whl -
Subject digest:
684e3de09f573250f27e8bc148db00f93ba7a14d923e9cec05939051f39ac1ec - Sigstore transparency entry: 1804271110
- Sigstore integration time:
-
Permalink:
islam666/Audnet@67e8d25e8f182d3e01d440134997802e4741563a -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/islam666
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@67e8d25e8f182d3e01d440134997802e4741563a -
Trigger Event:
push
-
Statement type: