f5-veil 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- f5_veil-1.2.0/.gitignore +88 -0
- f5_veil-1.2.0/CHANGELOG.md +167 -0
- f5_veil-1.2.0/DISCLAIMER.md +29 -0
- f5_veil-1.2.0/LICENSE +21 -0
- f5_veil-1.2.0/PKG-INFO +341 -0
- f5_veil-1.2.0/README.md +309 -0
- f5_veil-1.2.0/SECURITY.md +41 -0
- f5_veil-1.2.0/docs/architecture.md +802 -0
- f5_veil-1.2.0/pyproject.toml +69 -0
- f5_veil-1.2.0/requirements.txt +1 -0
- f5_veil-1.2.0/src/veil/__init__.py +3 -0
- f5_veil-1.2.0/src/veil/__main__.py +8 -0
- f5_veil-1.2.0/src/veil/ad_dn_discovery.py +186 -0
- f5_veil-1.2.0/src/veil/answer_file.py +382 -0
- f5_veil-1.2.0/src/veil/apm_var_literal_discovery.py +115 -0
- f5_veil-1.2.0/src/veil/cert_keychain_discovery.py +131 -0
- f5_veil-1.2.0/src/veil/cli.py +778 -0
- f5_veil-1.2.0/src/veil/client_policy_discovery.py +122 -0
- f5_veil-1.2.0/src/veil/data_group_records_discovery.py +173 -0
- f5_veil-1.2.0/src/veil/description_discovery.py +146 -0
- f5_veil-1.2.0/src/veil/diagnostics.py +56 -0
- f5_veil-1.2.0/src/veil/fqdn_discovery.py +110 -0
- f5_veil-1.2.0/src/veil/ip_discovery.py +225 -0
- f5_veil-1.2.0/src/veil/irule_comment_discovery.py +115 -0
- f5_veil-1.2.0/src/veil/krb_realm_discovery.py +106 -0
- f5_veil-1.2.0/src/veil/ldap_filter_discovery.py +168 -0
- f5_veil-1.2.0/src/veil/leak_detector.py +496 -0
- f5_veil-1.2.0/src/veil/ledger.py +569 -0
- f5_veil-1.2.0/src/veil/monitor_recv_discovery.py +96 -0
- f5_veil-1.2.0/src/veil/remote_role_discovery.py +198 -0
- f5_veil-1.2.0/src/veil/saml_oauth_discovery.py +171 -0
- f5_veil-1.2.0/src/veil/scanner.py +652 -0
- f5_veil-1.2.0/src/veil/snmp_discovery.py +338 -0
- f5_veil-1.2.0/src/veil/sshd_discovery.py +157 -0
- f5_veil-1.2.0/src/veil/substitute.py +1086 -0
- f5_veil-1.2.0/src/veil/syslog_discovery.py +196 -0
- f5_veil-1.2.0/src/veil/tokenizer.py +93 -0
- f5_veil-1.2.0/src/veil/ucs_archive.py +211 -0
- f5_veil-1.2.0/src/veil/username_discovery.py +118 -0
- f5_veil-1.2.0/tests/.gitkeep +0 -0
- f5_veil-1.2.0/tests/README.md +69 -0
- f5_veil-1.2.0/tests/conftest.py +6 -0
- f5_veil-1.2.0/tests/test_ad_dn_bareword_redaction.py +235 -0
- f5_veil-1.2.0/tests/test_ad_dn_redaction.py +308 -0
- f5_veil-1.2.0/tests/test_answer_file.py +369 -0
- f5_veil-1.2.0/tests/test_apm_firewall_kinds.py +147 -0
- f5_veil-1.2.0/tests/test_apm_var_literal_redaction.py +155 -0
- f5_veil-1.2.0/tests/test_bareword_infix_redaction.py +250 -0
- f5_veil-1.2.0/tests/test_caption_servicename_redaction.py +106 -0
- f5_veil-1.2.0/tests/test_cert_keychain_redaction.py +216 -0
- f5_veil-1.2.0/tests/test_cli.py +553 -0
- f5_veil-1.2.0/tests/test_cli_multi_file.py +269 -0
- f5_veil-1.2.0/tests/test_cli_ucs.py +399 -0
- f5_veil-1.2.0/tests/test_client_policy_redaction.py +185 -0
- f5_veil-1.2.0/tests/test_data_group_records_redaction.py +172 -0
- f5_veil-1.2.0/tests/test_description_redaction.py +266 -0
- f5_veil-1.2.0/tests/test_filestore_colon_redaction.py +100 -0
- f5_veil-1.2.0/tests/test_fqdn_leaf_form_redaction.py +121 -0
- f5_veil-1.2.0/tests/test_fqdn_redaction.py +271 -0
- f5_veil-1.2.0/tests/test_gtm_kinds.py +214 -0
- f5_veil-1.2.0/tests/test_integration_real_configs.py +483 -0
- f5_veil-1.2.0/tests/test_ip_discovery.py +226 -0
- f5_veil-1.2.0/tests/test_ip_substitution.py +238 -0
- f5_veil-1.2.0/tests/test_irule_tcl_redaction.py +530 -0
- f5_veil-1.2.0/tests/test_krb_realm_redaction.py +165 -0
- f5_veil-1.2.0/tests/test_ldap_filter_redaction.py +174 -0
- f5_veil-1.2.0/tests/test_leak_detector.py +374 -0
- f5_veil-1.2.0/tests/test_ledger.py +130 -0
- f5_veil-1.2.0/tests/test_ltm_extras_kinds.py +237 -0
- f5_veil-1.2.0/tests/test_monitor_recv_redaction.py +109 -0
- f5_veil-1.2.0/tests/test_net_and_gtm_region_kinds.py +82 -0
- f5_veil-1.2.0/tests/test_oauth_key_id_redaction.py +66 -0
- f5_veil-1.2.0/tests/test_profile_kind.py +208 -0
- f5_veil-1.2.0/tests/test_qstring_wrapped_header_paths.py +166 -0
- f5_veil-1.2.0/tests/test_remote_role_redaction.py +279 -0
- f5_veil-1.2.0/tests/test_saml_oauth_redaction.py +216 -0
- f5_veil-1.2.0/tests/test_scan_many.py +273 -0
- f5_veil-1.2.0/tests/test_scanner.py +148 -0
- f5_veil-1.2.0/tests/test_snmp_redaction.py +396 -0
- f5_veil-1.2.0/tests/test_sshd_banner_redaction.py +238 -0
- f5_veil-1.2.0/tests/test_substitute.py +446 -0
- f5_veil-1.2.0/tests/test_syslog_redaction.py +236 -0
- f5_veil-1.2.0/tests/test_tokenizer.py +44 -0
- f5_veil-1.2.0/tests/test_ucs_archive.py +305 -0
- f5_veil-1.2.0/tests/test_username_redaction.py +208 -0
- f5_veil-1.2.0/tests/test_version_field_skip.py +115 -0
f5_veil-1.2.0/.gitignore
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# VEIL-specific — NEVER commit obfuscation artifacts
|
|
3
|
+
# ============================================================================
|
|
4
|
+
*.answers.enc
|
|
5
|
+
*.sanitized.conf
|
|
6
|
+
*.sanitized.tcl
|
|
7
|
+
*.sanitized.ucs
|
|
8
|
+
*.restored.conf
|
|
9
|
+
test_configs/customer/
|
|
10
|
+
test_configs/real/
|
|
11
|
+
test_configs/_phase_verify/
|
|
12
|
+
*.ucs
|
|
13
|
+
|
|
14
|
+
# ============================================================================
|
|
15
|
+
# Session / handoff state — may contain personal network info, hostnames,
|
|
16
|
+
# customer device names, etc. Used to prime new Claude Code sessions; not
|
|
17
|
+
# part of the project history. NEVER commit.
|
|
18
|
+
# ============================================================================
|
|
19
|
+
BRIDGE_NEXT_SESSION.md
|
|
20
|
+
V12_LEAK_FIX_PLAN.md
|
|
21
|
+
CLAUDE.md
|
|
22
|
+
.claude/
|
|
23
|
+
|
|
24
|
+
# ============================================================================
|
|
25
|
+
# Python
|
|
26
|
+
# ============================================================================
|
|
27
|
+
__pycache__/
|
|
28
|
+
*.py[cod]
|
|
29
|
+
*$py.class
|
|
30
|
+
*.so
|
|
31
|
+
.Python
|
|
32
|
+
build/
|
|
33
|
+
develop-eggs/
|
|
34
|
+
dist/
|
|
35
|
+
downloads/
|
|
36
|
+
eggs/
|
|
37
|
+
.eggs/
|
|
38
|
+
lib/
|
|
39
|
+
lib64/
|
|
40
|
+
parts/
|
|
41
|
+
sdist/
|
|
42
|
+
var/
|
|
43
|
+
wheels/
|
|
44
|
+
share/python-wheels/
|
|
45
|
+
*.egg-info/
|
|
46
|
+
.installed.cfg
|
|
47
|
+
*.egg
|
|
48
|
+
MANIFEST
|
|
49
|
+
|
|
50
|
+
# Virtual environments
|
|
51
|
+
.env
|
|
52
|
+
.venv
|
|
53
|
+
env/
|
|
54
|
+
venv/
|
|
55
|
+
ENV/
|
|
56
|
+
|
|
57
|
+
# Testing
|
|
58
|
+
.coverage
|
|
59
|
+
.coverage.*
|
|
60
|
+
.pytest_cache/
|
|
61
|
+
htmlcov/
|
|
62
|
+
.tox/
|
|
63
|
+
.nox/
|
|
64
|
+
coverage.xml
|
|
65
|
+
*.cover
|
|
66
|
+
.hypothesis/
|
|
67
|
+
|
|
68
|
+
# Type checkers
|
|
69
|
+
.mypy_cache/
|
|
70
|
+
.pyre/
|
|
71
|
+
.pytype/
|
|
72
|
+
.ruff_cache/
|
|
73
|
+
|
|
74
|
+
# ============================================================================
|
|
75
|
+
# IDE / Editor
|
|
76
|
+
# ============================================================================
|
|
77
|
+
.vscode/
|
|
78
|
+
.idea/
|
|
79
|
+
*.swp
|
|
80
|
+
*.swo
|
|
81
|
+
*~
|
|
82
|
+
|
|
83
|
+
# ============================================================================
|
|
84
|
+
# OS
|
|
85
|
+
# ============================================================================
|
|
86
|
+
.DS_Store
|
|
87
|
+
Thumbs.db
|
|
88
|
+
desktop.ini
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **f5-veil** are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.2.0] — 2026-06-16
|
|
9
|
+
|
|
10
|
+
### Added — input-source expansions
|
|
11
|
+
|
|
12
|
+
- **Multi-file two-pass ingestion** for `bigip_base.conf` +
|
|
13
|
+
`bigip.conf` pairs. The base file's objects (VLANs, self-IPs,
|
|
14
|
+
route-domains, etc.) are registered in a shared ledger before the
|
|
15
|
+
main file's references need to resolve. New `scan_many` API; new
|
|
16
|
+
CLI `--input <path>` repeatable flag plus `--output-dir`.
|
|
17
|
+
- **UCS archive ingestion** (extract-only). CLI auto-detects `.ucs`
|
|
18
|
+
input, extracts the allowlist members (`config/bigip_base.conf`,
|
|
19
|
+
`config/bigip.conf`, optional `config/bigip_user.conf`),
|
|
20
|
+
obfuscates each, writes to `--output-dir`. UCS is never modified
|
|
21
|
+
or re-packed; allowlist excludes `bigip_script.conf` (deferred —
|
|
22
|
+
iApp templates collide with the IP placeholder model; tracked for
|
|
23
|
+
v1.3 / v2.0).
|
|
24
|
+
|
|
25
|
+
### Added — leak-coverage hardening (19 finding-groups + follow-ups)
|
|
26
|
+
|
|
27
|
+
Driven by real-corpus manual inspection. Approximately 28 new
|
|
28
|
+
`Kind` values across 12 new walkers.
|
|
29
|
+
|
|
30
|
+
- **Unknown top-level body walkers** — closes the brace-skip gap on
|
|
31
|
+
blocks whose body content carries customer-identifying data:
|
|
32
|
+
- `sys snmp` — community / trap bucket headers, plaintext
|
|
33
|
+
community strings, `sys-contact`, `sys-location`
|
|
34
|
+
(`SNMP_COMMUNITY`, `SNMP_TRAP`, `SNMP_COMMUNITY_SECRET`,
|
|
35
|
+
`SYS_CONTACT`, `SYS_LOCATION`)
|
|
36
|
+
- `sys syslog` — remote-server bucket headers (`SYSLOG_SERVER`)
|
|
37
|
+
- `sys sshd` — banner text (multi-line QSTRING; `SSHD_BANNER`)
|
|
38
|
+
- `auth remote-role role-info` — bucket-path discovery
|
|
39
|
+
(`REMOTE_ROLE`)
|
|
40
|
+
- **Nested-bucket walkers inside known top-level kinds** — the
|
|
41
|
+
enclosing kind's body is brace-skipped by the main loop, so
|
|
42
|
+
nested-object bareword names leak:
|
|
43
|
+
- `cert-key-chain` bucket names inside `ltm profile client-ssl`
|
|
44
|
+
bodies (`CERT_KEY_CHAIN`)
|
|
45
|
+
- `client-policy` bucket names inside APM profile bodies
|
|
46
|
+
(`CLIENT_POLICY`)
|
|
47
|
+
- **Cross-cutting field walkers**:
|
|
48
|
+
- Identity / hostname — `admin-name`, `basic-auth-username`,
|
|
49
|
+
`basic-auth-realm`, `user`, `account-name`, `server-name` →
|
|
50
|
+
`USERNAME`
|
|
51
|
+
- Kerberos realm — `realm` field with uppercase value (catches
|
|
52
|
+
public-TLD realms like `BOGUS.COM` that the FQDN walker by
|
|
53
|
+
design skips) → `KRB_REALM`
|
|
54
|
+
- LDAP filter — `filter` field inside LDAP-flavoured blocks →
|
|
55
|
+
`LDAP_FILTER`
|
|
56
|
+
- LDAP base-DN bareword — extends `AD_GROUP_DN` walker to catch
|
|
57
|
+
bareword `DC=...,DC=...` values in `base-dn` /
|
|
58
|
+
`search-base-dn` fields
|
|
59
|
+
- SAML / OAuth identifiers — `entity-id`, `sso-uri`,
|
|
60
|
+
`single-logout-uri`, `single-logout-response-uri`, `audience`
|
|
61
|
+
(braced-list form), `issuer`, `key-id` as dedicated kinds so
|
|
62
|
+
non-FQDN-shaped opaque values are caught (`SAML_ENTITY_ID`,
|
|
63
|
+
`SAML_SSO_URI`, `SAML_SLO_URI`, `SAML_SLO_RESPONSE_URI`,
|
|
64
|
+
`OAUTH_AUDIENCE`, `OAUTH_ISSUER`, `OAUTH_KEY_ID`)
|
|
65
|
+
- `caption` and `service-name` folded into the `DESC` walker
|
|
66
|
+
- Monitor `recv` strings — `MONITOR_RECV`
|
|
67
|
+
- Data-group `records` bucket headers (context-gated, catches
|
|
68
|
+
public-TLD entries that the global FQDN walker skips) —
|
|
69
|
+
`DATA_GROUP_RECORD`
|
|
70
|
+
- APM `expression "return {LITERAL}"` Tcl-literal pattern in
|
|
71
|
+
`variable-assign` bodies — `APM_VAR_LITERAL`
|
|
72
|
+
- **Substring-substitution variants**:
|
|
73
|
+
- F5 filestore colon-separator
|
|
74
|
+
(`:Common:<leaf>_<index>_<index>`) — covers `cache-path`
|
|
75
|
+
references that the slash-form substring sub missed
|
|
76
|
+
- FQDN-shaped leaf form for path-shape entries whose leaf is
|
|
77
|
+
public-TLD — covers `source-path /config/ssl/ssl.csr/<fqdn>.com`
|
|
78
|
+
references
|
|
79
|
+
- QSTRING-wrapped header path detection — catches bot-defense
|
|
80
|
+
signature path shapes
|
|
81
|
+
- Per-kind right-boundary protection (FQDN compound filenames)
|
|
82
|
+
- **Fixes**:
|
|
83
|
+
- IP-walker version-field exclusion — `version 17.5.1.5` no
|
|
84
|
+
longer gets substituted as if it were an IPv4 address
|
|
85
|
+
- AD_GROUP_DN qualifier relaxation — drop CN= requirement so
|
|
86
|
+
OU-prefix DNs (LDAP `base` values) are redacted as a whole,
|
|
87
|
+
not just the DC suffix
|
|
88
|
+
- Orphan-check substring-shadow exemption — FQDN entries
|
|
89
|
+
intentionally shadowed by longer SAML/OAuth ledger entries
|
|
90
|
+
don't trip the cross-reference integrity assertion
|
|
91
|
+
|
|
92
|
+
### Verification
|
|
93
|
+
|
|
94
|
+
Real-corpus canary count for the integration pair (homelab
|
|
95
|
+
AD-domain root-label, case-insensitive grep) went from **40 → 0**
|
|
96
|
+
across the v1.2 cycle. Full test suite: **503 → 660+** tests
|
|
97
|
+
passing, zero regressions, byte-exact round-trip preserved.
|
|
98
|
+
|
|
99
|
+
### Known gaps (documented, operator review required)
|
|
100
|
+
|
|
101
|
+
See [docs/architecture.md](docs/architecture.md) for the full
|
|
102
|
+
list. Highlights:
|
|
103
|
+
|
|
104
|
+
- iRule `varname` customer-name leaks
|
|
105
|
+
- Public-TLD FQDNs outside the dedicated walker / cert-path /
|
|
106
|
+
source-path contexts
|
|
107
|
+
- Free-text Tcl expression literals (`expression "[mcget {...}]"`)
|
|
108
|
+
without a recognised shape
|
|
109
|
+
|
|
110
|
+
## [1.1.1] — never published to PyPI
|
|
111
|
+
|
|
112
|
+
Corrective license swap — standard MIT + non-binding `DISCLAIMER.md`
|
|
113
|
+
replaces the prior "MIT-Modified Named-Party Exclusion" language.
|
|
114
|
+
PyPI republish was deferred; v1.1.1's content is included in v1.2.0.
|
|
115
|
+
|
|
116
|
+
## [1.1.0] — pushed 2026-06-13
|
|
117
|
+
|
|
118
|
+
### Added
|
|
119
|
+
|
|
120
|
+
- **BAREWORD infix substring substitution** — catches identifiers
|
|
121
|
+
embedded in compound barewords. Examples:
|
|
122
|
+
- `application-uri https://10.0.0.42/path` (IP inside URL)
|
|
123
|
+
- `iRule references like /Common/web1:80` (path inside compound)
|
|
124
|
+
- IP ranges like `10.0.0.1-10.0.0.50`
|
|
125
|
+
- File-storage compound filenames `<fqdn>_<index>_<index>`
|
|
126
|
+
- Word-boundary protection prevents partial matches against longer
|
|
127
|
+
numeric / identifier runs.
|
|
128
|
+
|
|
129
|
+
## [1.0.0] — pushed 2026-06-13
|
|
130
|
+
|
|
131
|
+
First stable release. Production-shaped against real BIG-IP
|
|
132
|
+
configurations from a controlled-environment lab corpus.
|
|
133
|
+
|
|
134
|
+
### Added
|
|
135
|
+
|
|
136
|
+
- **CLI**: `veil obfuscate` and `veil deobfuscate` commands. Exit
|
|
137
|
+
codes 0 (success), 2 (CLI usage), 3 (input not readable),
|
|
138
|
+
4 (diagnostics non-empty without `--allow-incomplete`),
|
|
139
|
+
5 (leak detector tripped under `--strict`).
|
|
140
|
+
- **Answer file**: AES-256-GCM-encrypted, scrypt KDF, atomic
|
|
141
|
+
writes.
|
|
142
|
+
- **Path-shape kinds**: pool, virtual server, node, monitor, iRule,
|
|
143
|
+
partition (LTM); pool, wide-IP, server, datacenter, region (GTM);
|
|
144
|
+
VLAN, route-domain, self-IP, trunk (net); policy, profile (APM);
|
|
145
|
+
policy, rule-list, address-list, port-list (security firewall);
|
|
146
|
+
data-group, SNAT, SNAT pool, virtual-address (LTM extras);
|
|
147
|
+
profile (LTM, with factory built-in exemption); UNKNOWN
|
|
148
|
+
best-effort registration for unrecognised top-level blocks.
|
|
149
|
+
- **IP literal handling**: bare IPv4 / IPv6 substituted into RFC
|
|
150
|
+
5737 / RFC 3849 docs ranges, preserving source `/24` and `/64`
|
|
151
|
+
structure first-seen-first-allocated.
|
|
152
|
+
- **Free-text**: description bodies (QSTRING, bareword, braced),
|
|
153
|
+
Tcl `#` comments inside `ltm rule` bodies, LDAP / AD distinguished
|
|
154
|
+
names (`CN=...,DC=...`) inside any QSTRING, internal-FQDN
|
|
155
|
+
discovery (`*.local`, `*.corp`, `*.lan`, `*.internal`,
|
|
156
|
+
`*.intranet`, `*.home.arpa`, `*.private`).
|
|
157
|
+
- **Leak detector**: post-substitution check that flags common
|
|
158
|
+
patterns (RFC1918 / CGNAT / link-local IPs, internal FQDNs, MAC
|
|
159
|
+
addresses, identifier-shaped barewords, paths with non-safe
|
|
160
|
+
partitions).
|
|
161
|
+
- **Strict mode**: `--strict` aborts on any leak-detector warning.
|
|
162
|
+
|
|
163
|
+
## Pre-1.0
|
|
164
|
+
|
|
165
|
+
Versions 0.0.1 through 0.0.14 were development-cycle iterations.
|
|
166
|
+
The detailed history is captured in the git log; consult
|
|
167
|
+
`git log --oneline` for per-commit context.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# DISCLAIMER
|
|
2
|
+
|
|
3
|
+
## Software License
|
|
4
|
+
|
|
5
|
+
This project is released under the **MIT License**. The full text of that
|
|
6
|
+
license is in [LICENSE](LICENSE) and is the only legally binding license
|
|
7
|
+
governing your use, modification, and redistribution of this software.
|
|
8
|
+
|
|
9
|
+
## Personal Statement of Intent
|
|
10
|
+
|
|
11
|
+
The following is a personal statement from the author. It is **not** a
|
|
12
|
+
software license term, **not** an additional restriction under the MIT
|
|
13
|
+
License, and **not** enforceable as a condition of use. It is provided here
|
|
14
|
+
only as a statement of the author's personal preferences regarding the
|
|
15
|
+
audience of this work.
|
|
16
|
+
|
|
17
|
+
> The author would prefer that this software not be used, redistributed, or
|
|
18
|
+
> incorporated by Austin Geraci or by WorldTech IT (or by any employee,
|
|
19
|
+
> agent, contractor, consultant, or affiliate acting on their behalf, or by
|
|
20
|
+
> any entity they own, operate, or control).
|
|
21
|
+
>
|
|
22
|
+
> This preference does not modify the MIT license grant. Any party covered
|
|
23
|
+
> by the MIT license remains licensed under its terms.
|
|
24
|
+
>
|
|
25
|
+
> Inspired by Stewart Semple's Black 3.0 paint license.
|
|
26
|
+
|
|
27
|
+
If you are evaluating this project for inclusion in another work, for
|
|
28
|
+
packaging, or for compliance review: treat the MIT license file as
|
|
29
|
+
authoritative and ignore the statement above for legal purposes.
|
f5_veil-1.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Blake Deakins
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to
|
|
7
|
+
deal in the Software without restriction, including without limitation the
|
|
8
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
9
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
21
|
+
IN THE SOFTWARE.
|
f5_veil-1.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: f5-veil
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: F5 BIG-IP config obfuscator/de-obfuscator — sanitize customer configs for safe AI analysis
|
|
5
|
+
Project-URL: Homepage, https://github.com/BDeakins/f5-veil
|
|
6
|
+
Project-URL: Issues, https://github.com/BDeakins/f5-veil/issues
|
|
7
|
+
Project-URL: Source, https://github.com/BDeakins/f5-veil
|
|
8
|
+
Author: Blake Deakins
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-safety,bigip,f5,irules,obfuscation,redaction,security,tmos
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: System :: Networking
|
|
23
|
+
Classifier: Topic :: System :: Systems Administration
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: cryptography>=42.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# f5-veil
|
|
34
|
+
|
|
35
|
+
F5 BIG-IP config obfuscator / de-obfuscator — sanitize customer configs
|
|
36
|
+
for safe AI analysis, then restore identifiers byte-exactly after the
|
|
37
|
+
AI is done.
|
|
38
|
+
|
|
39
|
+
## Status
|
|
40
|
+
|
|
41
|
+
**v1.2** — production-shaped against real BIG-IP configurations.
|
|
42
|
+
|
|
43
|
+
Covers ~50 object kinds across LTM, GTM, net, APM, sys, security
|
|
44
|
+
firewall, and SAML/OAuth/Kerberos/SNMP/syslog/SSHD bodies. Bare
|
|
45
|
+
IPv4 / IPv6 literals substituted to RFC 5737 / RFC 3849 docs ranges
|
|
46
|
+
with `/24` and `/64` source-subnet preservation. All three
|
|
47
|
+
description body forms (QSTRING, bareword, braced) plus `caption`
|
|
48
|
+
and `service-name` fields redacted. Tcl `#` comments inside `ltm
|
|
49
|
+
rule` bodies redacted. Identifier substring substitution inside
|
|
50
|
+
every QSTRING **and every BAREWORD** (catches monitor send/recv
|
|
51
|
+
strings, APM policy expressions, bot-defense signatures, URL-shaped
|
|
52
|
+
barewords, IP ranges, F5 filestore colon-separator paths
|
|
53
|
+
(`:Common:<leaf>_<index>_<index>`), public-TLD FQDN leafs in
|
|
54
|
+
source-paths). LDAP / AD distinguished names embedded in any
|
|
55
|
+
QSTRING **and** as bareword `base-dn` / `search-base-dn` values.
|
|
56
|
+
Kerberos realms (uppercase form, public-TLD support). SAML / OAuth
|
|
57
|
+
identifier fields (entity-id, sso-uri, slo-uri, audience, issuer,
|
|
58
|
+
key-id) as dedicated kinds — non-FQDN-shaped opaque values are
|
|
59
|
+
caught. APM `expression "return {LITERAL}"` Tcl-literal patterns
|
|
60
|
+
catch hard-coded session-variable values (domains, usernames,
|
|
61
|
+
occasionally credentials). Multi-file two-pass ingestion
|
|
62
|
+
(`bigip_base.conf` + `bigip.conf`). UCS archive ingestion
|
|
63
|
+
(extract-only). AES-256-GCM-encrypted answer file with scrypt KDF.
|
|
64
|
+
Round-trip is byte-exact for every shape the parser covers.
|
|
65
|
+
|
|
66
|
+
Real-corpus canary count for the v1.2 integration pair went from
|
|
67
|
+
40 → 0 across the v1.2 leak-coverage cycle (19 finding-groups
|
|
68
|
+
discovered via manual inspection plus post-sign-off follow-ups).
|
|
69
|
+
660+ tests pass with byte-exact round-trip preserved.
|
|
70
|
+
|
|
71
|
+
Documented gaps (see [docs/architecture.md](docs/architecture.md)
|
|
72
|
+
"Known gaps"): iRule `varname` customer leaks, public-TLD FQDNs
|
|
73
|
+
outside cert/path/SAML contexts, free-text Tcl expression literals
|
|
74
|
+
without recognised shape.
|
|
75
|
+
|
|
76
|
+
## The Problem
|
|
77
|
+
|
|
78
|
+
F5 engineers want to use AI tools (Claude, ChatGPT, Copilot, etc.) to
|
|
79
|
+
analyze configurations, write iRules, and troubleshoot issues. But
|
|
80
|
+
customer configurations contain identifying information — IPs,
|
|
81
|
+
hostnames, pool names, virtual server names, monitor names, AD group
|
|
82
|
+
DNs, partition labels — that cannot legally or contractually be sent
|
|
83
|
+
to a third-party AI under most customer NDAs and employer policies.
|
|
84
|
+
The penalty for leaking customer data to an AI is often immediate
|
|
85
|
+
termination.
|
|
86
|
+
|
|
87
|
+
## What VEIL does
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
veil obfuscate → sanitized.conf + answers.enc (encrypted)
|
|
91
|
+
→ safe to paste into AI
|
|
92
|
+
|
|
93
|
+
[engineer collaborates with AI on the sanitized config]
|
|
94
|
+
|
|
95
|
+
veil deobfuscate → restored.conf (real identifiers reinstated,
|
|
96
|
+
including in any new content the AI generated)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Every customer-identifying value gets a typed placeholder
|
|
100
|
+
(`POOL_0001`, `VS_0001`, `NODE_0001`, `IRULE_0001`, `DESC_0001`,
|
|
101
|
+
`AD_GROUP_DN_0001`, `SAML_ENTITY_ID_0001`, `SNMP_COMMUNITY_SECRET_0001`,
|
|
102
|
+
`SSHD_BANNER_0001`, `USERNAME_0001`, `APM_VAR_LITERAL_0001`, etc.),
|
|
103
|
+
the original bytes go into an encrypted answer file, and the
|
|
104
|
+
de-obfuscator restores everything byte-exactly — including any
|
|
105
|
+
placeholder text the AI produced in new content it wrote.
|
|
106
|
+
|
|
107
|
+
## Safety warnings
|
|
108
|
+
|
|
109
|
+
> **VEIL is a safety net, not a guarantee.**
|
|
110
|
+
> A parser miss = customer data leaked to an LLM = potential
|
|
111
|
+
> career-ending incident. Always review the sanitized output before
|
|
112
|
+
> sending it anywhere.
|
|
113
|
+
|
|
114
|
+
- Read the sanitized file end-to-end before sending it to AI.
|
|
115
|
+
- The leak detector flags common patterns (RFC1918 IPs, `.local` /
|
|
116
|
+
`.corp` / `.lan` / `.internal` domains, MAC addresses,
|
|
117
|
+
identifier-shaped barewords, paths with non-safe partitions). It is
|
|
118
|
+
heuristic — a clean run is strong evidence, not proof.
|
|
119
|
+
- Use `--strict` mode to abort on any leak-detector warning.
|
|
120
|
+
- Use `--allow-incomplete` only when you understand exactly which kinds
|
|
121
|
+
the parser doesn't yet recognise.
|
|
122
|
+
- Protect the answer file as you would a UCS archive. Anyone with the
|
|
123
|
+
file and the passphrase can recover the original configuration.
|
|
124
|
+
- Never commit `*.answers.enc` or `*.sanitized.conf` to a repo. The
|
|
125
|
+
shipped `.gitignore` blocks both — keep it that way.
|
|
126
|
+
- VEIL does not attempt to obfuscate inside binary blobs, base64-encoded
|
|
127
|
+
archives, or compiled artifacts. Strip those before obfuscation.
|
|
128
|
+
|
|
129
|
+
## Installation
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install f5-veil
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Or from source:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git clone https://github.com/BDeakins/f5-veil
|
|
139
|
+
cd f5-veil
|
|
140
|
+
pip install -e .
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Requires Python 3.10 or newer.
|
|
144
|
+
|
|
145
|
+
## Usage
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Obfuscate a single bigip.conf
|
|
149
|
+
veil obfuscate --input bigip.conf \
|
|
150
|
+
--output bigip.sanitized.conf \
|
|
151
|
+
--answer-file bigip.answers.enc
|
|
152
|
+
|
|
153
|
+
# De-obfuscate (AI may have introduced new content; placeholders inside
|
|
154
|
+
# new content are restored too)
|
|
155
|
+
veil deobfuscate --input bigip.modified.conf \
|
|
156
|
+
--output bigip.restored.conf \
|
|
157
|
+
--answer-file bigip.answers.enc
|
|
158
|
+
|
|
159
|
+
# Dry-run obfuscation — report what would change, write nothing
|
|
160
|
+
veil obfuscate --input bigip.conf --dry-run
|
|
161
|
+
|
|
162
|
+
# Strict mode — abort if the leak detector finds anything suspicious
|
|
163
|
+
veil obfuscate --input bigip.conf --strict ...
|
|
164
|
+
|
|
165
|
+
# Allow-incomplete mode — proceed even with unhandled top-level blocks
|
|
166
|
+
# (e.g. ltm dns, security dos). Use only when you've reviewed the
|
|
167
|
+
# diagnostics and understand the residual leak surface.
|
|
168
|
+
veil obfuscate --input bigip.conf --allow-incomplete ...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Multi-file mode (`bigip_base.conf` + `bigip.conf`)
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Pass both files; base file first so its objects (VLANs, self-IPs,
|
|
175
|
+
# route domains) land in the ledger before the main file's references
|
|
176
|
+
# need to resolve. Output goes to a directory keyed by basename.
|
|
177
|
+
veil obfuscate --input bigip_base.conf \
|
|
178
|
+
--input bigip.conf \
|
|
179
|
+
--output-dir sanitized/ \
|
|
180
|
+
--answer-file device.answers.enc
|
|
181
|
+
|
|
182
|
+
veil deobfuscate --input sanitized/bigip_base.conf \
|
|
183
|
+
--input sanitized/bigip.conf \
|
|
184
|
+
--output-dir restored/ \
|
|
185
|
+
--answer-file device.answers.enc
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`--input` order on the deobfuscate side must match the order recorded
|
|
189
|
+
in the answer file at obfuscation time. Reordering is a hard error,
|
|
190
|
+
not a silent miscorrelation.
|
|
191
|
+
|
|
192
|
+
### UCS archive mode (`device.ucs`)
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Hand VEIL the UCS directly. It extracts the allowlisted config-file
|
|
196
|
+
# members (config/bigip_base.conf, config/bigip.conf, and the
|
|
197
|
+
# optional config/bigip_user.conf), obfuscates each, and writes them
|
|
198
|
+
# as separate text files into --output-dir. Everything else in the
|
|
199
|
+
# UCS (bigip_script.conf, certs, keys, licenses, binaries, state
|
|
200
|
+
# files, .diffVersions snapshots) is ignored — never read, never
|
|
201
|
+
# written.
|
|
202
|
+
veil obfuscate --input device.ucs \
|
|
203
|
+
--output-dir sanitized/ \
|
|
204
|
+
--answer-file device.answers.enc
|
|
205
|
+
|
|
206
|
+
# Deobfuscate the sanitized text files via the standard multi-file
|
|
207
|
+
# flow. VEIL does NOT recreate the UCS — if you need a closed-loop
|
|
208
|
+
# UCS for restore, re-pack the restored files into the original
|
|
209
|
+
# archive yourself (e.g. with tar).
|
|
210
|
+
veil deobfuscate --input sanitized/bigip_base.conf \
|
|
211
|
+
--input sanitized/bigip.conf \
|
|
212
|
+
--input sanitized/bigip_user.conf \
|
|
213
|
+
--output-dir restored/ \
|
|
214
|
+
--answer-file device.answers.enc
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Note on `bigip_script.conf`:** the file containing iRules and
|
|
218
|
+
iApp templates is intentionally NOT in the v1.2 UCS allowlist —
|
|
219
|
+
its iApp template bodies contain literal RFC 5737 docs-range IPs
|
|
220
|
+
in user-facing help text that collide with VEIL's IP placeholder
|
|
221
|
+
model. See `docs/architecture.md` ("UCS archive ingestion") for the
|
|
222
|
+
threat model, allowlist rationale, and the architectural fix
|
|
223
|
+
planned for v1.3 / v2.0. If you need iRule / iApp coverage today,
|
|
224
|
+
hand `bigip_script.conf` to the LLM as a separate plain-text file.
|
|
225
|
+
|
|
226
|
+
Exit codes: 0 success, 2 CLI usage error, 3 input not readable, 4
|
|
227
|
+
diagnostics non-empty without `--allow-incomplete`, 5 leak detector
|
|
228
|
+
tripped under `--strict`.
|
|
229
|
+
|
|
230
|
+
## Identifier scope
|
|
231
|
+
|
|
232
|
+
**Obfuscated by VEIL (v1.2):**
|
|
233
|
+
|
|
234
|
+
- **LTM:** pool, virtual server, node, monitor, iRule, partition,
|
|
235
|
+
profile (custom — built-ins like `/Common/http` pass through as
|
|
236
|
+
universal BIG-IP signal), data-group name, data-group **records**
|
|
237
|
+
(operator-chosen lookup keys, even public-TLD ones), SNAT, SNAT
|
|
238
|
+
pool, virtual-address
|
|
239
|
+
- **GTM:** pool, wide-IP, server, datacenter, region
|
|
240
|
+
- **Net:** VLAN, route-domain, self-IP, trunk
|
|
241
|
+
- **APM:** policy, profile, `cert-key-chain` and `client-policy`
|
|
242
|
+
nested bucket names, `expression "return {LITERAL}"` Tcl literals
|
|
243
|
+
in `variable-assign` blocks
|
|
244
|
+
- **SAML / OAuth:** entity-id, sso-uri, single-logout-uri,
|
|
245
|
+
single-logout-response-uri, audience, issuer, key-id — dedicated
|
|
246
|
+
kinds so non-FQDN-shaped opaque values are caught (the FQDN
|
|
247
|
+
walker alone wouldn't catch URN entity-IDs or public-TLD URLs)
|
|
248
|
+
- **Identity / field walkers:** `admin-name`, `basic-auth-username`,
|
|
249
|
+
`basic-auth-realm`, `user`, `account-name`, `server-name` →
|
|
250
|
+
`USERNAME`; LDAP `filter` field; LDAP `base-dn` / `search-base-dn`
|
|
251
|
+
bareword DC=...,DC=... shapes
|
|
252
|
+
- **Sys family:** `sys snmp` body (community / trap bucket headers,
|
|
253
|
+
plaintext community strings, `sys-contact`, `sys-location`);
|
|
254
|
+
`sys syslog` remote-server bucket headers; `sys sshd` banner
|
|
255
|
+
text (multi-line QSTRING covered); `auth remote-role role-info`
|
|
256
|
+
bucket headers
|
|
257
|
+
- **Kerberos:** uppercase realm values (`ACME.CORP`, public TLDs
|
|
258
|
+
included — the FQDN walker by design only catches internal-suffix
|
|
259
|
+
realms)
|
|
260
|
+
- **Security firewall:** policy, rule-list, address-list, port-list
|
|
261
|
+
- **Network literals:** bare IPv4 / IPv6 (substituted into RFC 5737 /
|
|
262
|
+
RFC 3849 docs ranges, preserving source `/24` and `/64` structure
|
|
263
|
+
first-seen-first-allocated); IP-walker skips version-field values
|
|
264
|
+
(`version 17.5.1.5` no longer gets substituted as an IP)
|
|
265
|
+
- **Free-text:**
|
|
266
|
+
- `description` / `caption` / `service-name` bodies — QSTRING,
|
|
267
|
+
bareword, and braced forms all redacted to `DESC_NNNN`
|
|
268
|
+
- Tcl `#` comments inside `ltm rule` bodies — redacted to
|
|
269
|
+
`IRULE_COMMENT_NNNN`
|
|
270
|
+
- LDAP / AD distinguished names (`CN=...,DC=...` AND
|
|
271
|
+
`OU=...,DC=...`) anywhere inside any QSTRING — redacted to
|
|
272
|
+
`AD_GROUP_DN_NNNN`
|
|
273
|
+
- Internal-FQDN discovery (`*.local`, `*.corp`, `*.lan`,
|
|
274
|
+
`*.internal`, `*.intranet`, `*.home.arpa`, `*.private`) inside
|
|
275
|
+
any WORD or QSTRING — redacted to `FQDN_NNNN`
|
|
276
|
+
- Monitor `recv` strings (HTML titles, product names) — redacted
|
|
277
|
+
to `MONITOR_RECV_NNNN`
|
|
278
|
+
- F5 filestore colon-separator paths
|
|
279
|
+
(`:Common:<leaf>_<index>_<index>`) — caught via substring sub
|
|
280
|
+
variant on path-shape entries
|
|
281
|
+
- Any other ledger identifier appearing as a substring inside any
|
|
282
|
+
QSTRING / BAREWORD (monitor send-strings, APM policy
|
|
283
|
+
expressions, bot-defense signatures, URL-shaped barewords like
|
|
284
|
+
`https://10.0.0.42/path`, IP ranges like `10.0.0.1-10.0.0.50`)
|
|
285
|
+
— substring-substituted in place with word-boundary protection
|
|
286
|
+
- Multi-file mode: `bigip_base.conf` + `bigip.conf` ingest as a
|
|
287
|
+
shared ledger so base-file objects substitute correctly when
|
|
288
|
+
referenced from the main file
|
|
289
|
+
- UCS archive mode: extract-only, allowlists `config/bigip_base.conf`,
|
|
290
|
+
`config/bigip.conf`, `config/bigip_user.conf`
|
|
291
|
+
|
|
292
|
+
**Documented gaps (operator review required):**
|
|
293
|
+
|
|
294
|
+
- iRule `varname` customer-name leaks — renaming would break
|
|
295
|
+
positional Tcl refs, so VEIL does not auto-redact
|
|
296
|
+
- Public-TLD FQDNs outside the dedicated walker / cert-path /
|
|
297
|
+
source-path contexts — the global FQDN walker only catches
|
|
298
|
+
internal-suffix TLDs to avoid false positives on legitimate
|
|
299
|
+
public DNS references
|
|
300
|
+
- Free-text Tcl expression literals (`expression "[mcget {...}]"`)
|
|
301
|
+
without a recognised shape
|
|
302
|
+
- Persistent cross-run identifier map (deferred to v2.0)
|
|
303
|
+
- Folder-as-own-kind (`/Common/folder/sub/leaf` currently collapses
|
|
304
|
+
folder into the leaf placeholder) — v1.3+
|
|
305
|
+
|
|
306
|
+
## Roadmap
|
|
307
|
+
|
|
308
|
+
- **v1.0** — `bigip.conf` only. Shipped.
|
|
309
|
+
- **v1.1** — BAREWORD infix substring substitution (catches URLs,
|
|
310
|
+
IP ranges, compound barewords). Shipped.
|
|
311
|
+
- **v1.2** — `bigip_base.conf` multi-file two-pass discovery, UCS
|
|
312
|
+
archive ingestion (extract-only), `auth remote-role role-info`
|
|
313
|
+
bucket-path discovery, plus a 19-finding-group leak-coverage
|
|
314
|
+
hardening cycle driven by real-corpus manual inspection:
|
|
315
|
+
sys snmp / sys syslog / sys sshd body walkers, cert-key-chain and
|
|
316
|
+
client-policy nested bucket walkers, identity / Kerberos realm /
|
|
317
|
+
LDAP filter / SAML+OAuth / data-group-record / monitor recv /
|
|
318
|
+
APM expression literal field walkers, filestore colon-separator
|
|
319
|
+
substring sub, FQDN-shaped leaf substring sub. Real-corpus canary
|
|
320
|
+
count for the integration pair: 40 → 0. Shipped.
|
|
321
|
+
- **v1.3** — Personal-use Docker image + thin FastAPI wrapper around
|
|
322
|
+
the CLI (paste config in browser, get sanitized output and encrypted
|
|
323
|
+
answer file out). RAM-only processing, no auth, **not for internet
|
|
324
|
+
exposure**. CLI remains the canonical distribution. See the threat
|
|
325
|
+
model in [docs/architecture.md](docs/architecture.md).
|
|
326
|
+
- **v2.0** — Persistent cross-run identifier map (same source
|
|
327
|
+
identifier → same placeholder across runs, for ongoing engagements).
|
|
328
|
+
- **v2.1 / v3.0** — Hardened multi-user web service (auth, HTTPS,
|
|
329
|
+
audit logging, hard ephemerality guarantees, rate limiting). Shares
|
|
330
|
+
design surface with the v2.0 persistent map (auth + secret storage).
|
|
331
|
+
|
|
332
|
+
## License
|
|
333
|
+
|
|
334
|
+
MIT — see [LICENSE](LICENSE).
|
|
335
|
+
|
|
336
|
+
A personal statement of intent regarding the audience of this work is in
|
|
337
|
+
[DISCLAIMER.md](DISCLAIMER.md). It is not a license term.
|
|
338
|
+
|
|
339
|
+
## Security
|
|
340
|
+
|
|
341
|
+
See [SECURITY.md](SECURITY.md) for vulnerability reporting policy.
|