Stop guessing whether a converted IDS rule still means the same thing.
IDS Rule Converter is a secure, loss-aware, one-file Python toolkit for parsing, validating, analyzing, comparing, and converting Snort and Suricata rules. It defaults to refusing uncertain translations instead of silently dropping or weakening detection logic.
Project status: maintained public release. Version 4.0.2 is the current verified release. The converter supports documented Snort 2, Snort 3, and Suricata 8 lanes, but it does not promise that every engine feature has a lossless translation. Rejected and partial results are deliberate safety outcomes.
The runtime has no third-party Python dependencies. Copy
snort_suricata_rule_converter.py to a system with Python 3.12 or newer and run
it directly.
Version 4.0.2 is the current release. Download the verified standalone Python runtime or the deterministic documentation ZIP. The release page also provides a Python wheel and source archive, the SPDX SBOM, SHA-256 checksums, release evidence, and GitHub provenance. See RELEASING.md for the exact artifact and publication gates.
For an installed command, use the PyPI 4.0.2 package:
python -m pip install ids-rule-converter==4.0.2
ids-rule-converter --help
Rule conversion is not a keyword replacement problem. Sticky buffers, content modifier placement, service declarations, application protocols, regular expressions, and engine-specific actions all affect detection behavior.
IDS Rule Converter provides:
- Ordered parsing that preserves repeated options and content modifier context
- Strict, fail-safe conversion between Snort 2, Snort 3, and Suricata 8
- Explicit rejection files and machine-readable reports for unsafe rules
- Native JSON and SARIF output for automation and code scanning systems
- Duplicate SID, conflicting SID, keyword, protocol, and action analysis
- Semantic ruleset comparison by GID, SID, revision, and fingerprint
- Offline Panorama IPS Signature Converter 2.0.4 compatibility preflight
- Safe download support for allowlisted Cisco Talos community feeds
- Archive defenses against traversal, links, special files, duplicate paths, encrypted ZIP entries, Windows device names, and decompression abuse
- Atomic output writes, overwrite refusal, and input replacement protection
- No telemetry and no implicit network access
After installing the pinned PyPI package above, create a harmless synthetic rule and exercise the installed command:
@'
alert tcp any any -> any 443 (msg:"Synthetic TLS marker"; flow:to_server,established; content:"|16 03|"; depth:2; sid:1000001; rev:1;)
'@ | Set-Content -LiteralPath input.rules -Encoding utf8
ids-rule-converter --help
ids-rule-converter validate input.rules
ids-rule-converter analyze input.rules --output analysis.jsonConvert a Snort 3 ruleset to Suricata. Strict mode is the default, so no ruleset is written if any rule has an unsafe or unverified mapping.
ids-rule-converter convert input.rules --source-dialect snort3 --target suricata --output converted.rules --report conversion.jsonTo export only the verified subset, preserve every rejected source rule, and receive a detailed report:
ids-rule-converter convert input.rules --source-dialect snort3 --target suricata --output accepted.rules --allow-partial --rejected-output rejected.rules --report partial-conversion.jsonFrom a reviewed source checkout, replace ids-rule-converter with
python snort_suricata_rule_converter.py. If you downloaded the standalone
release asset instead, use python IDS-Rule-Converter-v4.0.2.py from the
directory containing that verified file.
An intentional partial result exits with code 2 so automation cannot mistake it for a complete conversion.
Reviewed partial conversion writes the current rejection file even when its count is zero. Primary/rejection comments and the JSON report share a generation ID; the report binds the intended UTF-8 artifacts by SHA-256. Verify those hashes before consuming the files. Each leaf is published atomically, but publication of the three files is not a single transaction.
Constructed test data. Strict mode writes no converted ruleset when one mapping is unsafe. An explicit partial export preserves accepted rules, rejected source rules, and the report as one review set. Native target validation is still required.
Detection engineers can contribute the most by sharing a minimal, anonymized rule whose semantics are unclear across engines. Remove organization names, internal addresses, proprietary intelligence, and licensed rule text before sharing. Include the source dialect, intended target, expected behavior, and native engine version in GitHub Discussions. A refused conversion is useful evidence when no safe mapping exists.
| Command | Purpose |
|---|---|
validate |
Parse rules and report structural or identifier problems |
analyze |
Inventory rules, keywords, actions, protocols, duplicates, and conflicts |
convert |
Convert to Snort 2, Snort 3, Suricata, or structured JSON |
panorama-preflight |
Check and batch rules for Panorama plugin 2.0.4 |
diff |
Compare two rulesets by GID and SID |
list-sources |
Show built-in HTTPS feed definitions |
fetch |
Download and optionally extract an allowlisted rule feed safely |
See QUICK_REFERENCE.md for copy-ready examples.
Auto detection rejects legacy buffer placement when the same text can mean a
Snort 2 backward content modifier or a Snort 3 forward sticky buffer. Use
--source-dialect snort2 or --source-dialect snort3 after identifying the
source engine. The tool does not guess which payload or HTTP buffer a pattern
should inspect. Panorama preflight also rejects these ambiguous rules.
Even with an explicit Snort 2 source, a backward buffer modifier must be
associated with its preceding content. A modifier separated by flow,
metadata, or another unrelated option is rejected for Snort 3 and Suricata,
including with --allow-unverified. It is never reinterpreted as a forward
selector for a later pattern.
Non-rule text after a closed rule on the same logical line is a parse error. The complete prefix is withheld, so conversion and Panorama cannot publish an artifact that silently drops trailing conditions. Separate complete rules on one line remain supported; standalone directives on separate lines are still reported as ignored text.
Snort 3 file_id supports its documented one-token header only. Protocol,
address, port or direction fields on that action are refused by the parser and
all public manual-rule/rendering/JSON paths. Ordinary network and service headers
retain their existing grammar.
Processing is bounded to 128 MiB per input, 1 MiB per rule, 256 options per
rule, 100,000 parsed rules, 1,000,000 total options, and 10,000 diagnostics.
Exceeding any parser budget aborts the operation, including with
--allow-partial, so a truncated prefix cannot be mistaken for a complete
ruleset. Archive object limits include implicit directories, with at most
32 path components. Console and text reports escape embedded control
characters; JSON retains the original values for review. Fingerprints preserve
content and PCRE literal whitespace rather than collapsing it.
Release verification binds package descriptions, authors, project URLs,
classifiers, README content, and the wheel generator to the reviewed source.
The protected verifier separately authorizes the installation contract as inert
TOML: exactly pinned setuptools==84.2.1 and wheel==0.48.0, the
setuptools.build_meta backend, the current static project identity, one runtime
module and its intended console script. Runtime dependency, optional dependency
and dynamic declarations must be absent or typed empty. Extra build selectors,
backend paths, package discovery, entry points and ambiguous requirements are
refused before distribution inspection. Future pin or version evolution requires
an explicit reviewed update to both this contract and pyproject.toml.
Source archives reject unreviewed empty directories and nonportable paths.
PyPI promotion verifies the copied distribution hashes and rechecks the tag,
signed commit, main ancestry, and public stable release after environment
approval. The runtime CLI uses only the standard library. Python 3.10 reached
end of life on October 1, 2026; repository source and future releases require
Python 3.12 through 3.14. The already-published v4.0.2 metadata cannot be
changed in place and still records its historical Python 3.10 compatibility.
The default behavior is intentionally conservative:
- The complete input must parse successfully.
- Each rule is checked against the declared target dialect.
- Verified transformations preserve option order and buffer context.
- A rule with an unsafe mapping is rejected, not approximated.
- Strict conversion writes nothing when any rule is rejected.
- Partial conversion requires both a rejection file and a JSON report.
- Unknown target keywords require the explicit
--allow-unverifiedopt-out.
Important verified transformations include Snort 3 and Suricata HTTP sticky
buffers, Snort 2 HTTP content modifiers, safe service mappings, TLS protocol
naming, selected SIP options, bufferlen to bsize, stream_size syntax,
fast-pattern offsets, and compatible tag syntax.
The converter rejects ambiguous multi-service rules, unsupported target actions, conflicting application protocols, unsafe buffer arguments, packet and application-layer conflicts, unsupported BER and DCE options, and other cases where equivalence has not been established.
Always run the target engine's native configuration test before deployment. Successful parsing proves that an engine accepts a rule. It does not prove that every rule will detect identical traffic under every engine configuration.
Explicit Suricata input keeps legacy HTTP content modifiers attached to the
preceding content when converting to Snort 3. Later payload matches restore
their payload buffer. Mixed sticky selectors and backward HTTP modifiers,
orphan modifiers, and cursor-dependent translations without a proven mapping
are rejected even with --allow-unverified. Suricata's underscore aliases for
forward selectors, such as http_protocol, remain distinct from backward HTTP
modifiers. See the Suricata HTTP syntax reference
and Snort 3 HTTP selectors.
Automatic dialect inference also rejects mixed underscore forward aliases and
backward HTTP groups; choose a source dialect only after inspecting the input.
Valued legacy buffer modifiers are refused when the requested conversion has
no proven value-preserving mapping, including conversions with
--allow-unverified. Snort 3 to Snort 3 retains its selector arguments, and
the supported Snort 3 http_header:field user-agent to Suricata
http.user_agent mapping remains available. That exception does not apply
to Snort 2 or Suricata backward modifiers.
The Suricata buffers dns_query, http_header_names, http_host,
http_protocol, http_raw_host, http_server_body and http_user_agent
(including dotted forms) have no proven Snort target mapping and are refused
even with --allow-unverified. Native Snort 3 validation identified these
unsupported selector names; this tool does not guess at a substitute field.
Panorama case checks track file_data and explicit payload-buffer transitions
for both content and PCRE. A file-data match requesting nocase is rejected
under the plugin's fixed case-sensitive file-data compatibility model; a
case-sensitive file-data match can pass the remaining checks. This is an
offline compatibility model, not execution against a Panorama appliance.
The Panorama command performs an offline compatibility review for IPS Signature Converter plugin 2.0.4. It checks documented action, protocol, condition, PCRE, threshold, reference, case-sensitivity, negation, and positional limits. Accepted source rules are divided into batches of no more than 100 rules and 8 MB.
python snort_suricata_rule_converter.py panorama-preflight input.rules --output-dir panorama-review
The tool never connects to Panorama and never uploads a rule.
Network access occurs only when fetch is explicitly invoked. Built-in sources
use HTTPS and an exact hostname allowlist. Redirects outside that allowlist are
refused.
python snort_suricata_rule_converter.py list-sources
python snort_suricata_rule_converter.py fetch --source snort3-community --output-dir downloads --extract
The downloaded archive, SHA-256 metadata, and extracted files are local outputs. Rules remain subject to their provider's terms and are not part of this project's Apache 2.0 license.
Fetch supervises a fresh Python spawn worker under one 30-second deadline, including DNS, connect, TLS, redirects, headers, body and byte-only IPC. On failure or interruption it cancels and reaps the worker. Operating-system process creation/termination latency remains outside the application deadline. Library callers must use normal import-safe guarded process startup; frozen executables and interactive embedding are not supported by this fetch supervisor. It trusts the installed interpreter, module and caller startup; it does not use isolated Python mode or execute selected feed code. On the main thread, a callable SIGINT handler is deferred during process and reader startup until both have been adopted for cleanup, then restored and invoked. Its original returning or raising behavior is preserved. Calls on other threads and ignored/default OS signal dispositions are unchanged. Arbitrary asynchronous exceptions and forced OS termination are outside this cleanup guarantee.
Extraction returns paths beneath a new generation-<id> directory after every
member has decoded successfully. The fetch metadata lists those completed paths.
ZIP file entities are independently counted and checksummed through complete
stored/raw-deflate input with bounded chunks before the standard extraction writer.
This changes the earlier direct-member output layout. Each call, including
force=True, preserves earlier generations and user files; it never overwrites
them. Unsupported atomic no-replace directory publication is refused. Failed
staging cleanup is limited to that newly created private generation and may
require owner review if its identity or contents no longer match.
TAR preflight also drains the bounded gzip stream through completion before
fetch creates its source-specific root. A failed extraction removes that
invocation's exclusively created root only when its recorded identity is intact
and it is empty after generation cleanup. Changed identity or remaining content
refuses removal and retains a cleanup diagnostic. A successfully published
generation is committed content and is preserved even if later artifact metadata
publication fails; existing source roots are never reused or removed implicitly.
Archive components must fit both 255 UTF-8 bytes and 255 Windows UTF-16 code
units. POSIX extraction also checks the retained destination descriptor's
PC_NAME_MAX and native component encoding before creating a stage; filesystems
without an established finite limit are refused. These limits apply to each
component, including names supplied through PAX. Previously accepted longer
component spellings are no longer supported.
Failure cleanup uses a reverse creation journal, including temporary atomic
writer files and private Windows writer directories. It checks each object's
type and recorded identity, attempts the remaining owned objects after an
individual removal failure, and reports aggregate failures while preserving
the initiating exception. An unavailable or changed identity still requires
owner review; cleanup never expands into unowned paths. Library exceptions
expose archive_cleanup_failures and archive_cleanup_failure_count; supported
Python versions also attach exception notes.
TAR PAX metadata is parsed under a separate object policy before member handling. Each extension admits at most 16 fields, each read pass admits at most 4,096 PAX fields in total, keywords have a 128-byte limit, and UTF-8 values have a 4,096-byte limit. Integer and decimal timestamp fields use a small bounded numeric grammar. Each pass also permits at most 100,000 effective field applications, including repeated global metadata and local overrides; a valid archive can therefore be refused below the ordinary member-count limit when it uses more metadata. The existing compressed/decompressed, extension-byte/chain, member, filesystem object, path, file-size and total extraction limits still apply.
Local PAX supports path, linkpath, size, uid, gid, uname, gname,
mtime, atime, ctime, hdrcharset and comment. Global PAX supports only
the owner, time, charset and comment fields; global path, linkpath and size
are refused. Charset metadata must specify ISO-IR 10646 2000 UTF-8. Its exact
raw byte range is checked before slicing or decoding the charset value, after
the existing length bound. Binary PAX, unknown vendor fields and every GNU sparse
PAX variant are refused before value decoding or sparse-map parsing. Links,
special files and legacy sparse members remain unsupported. Ordinary USTAR/GNU
names and common POSIX UTF-8 long paths
and fractional timestamps remain supported within these limits.
Empty numeric fields are refused rather than coerced to zero; empty paths still
fail path admission. Empty owner/comment strings remain text metadata. This is
a bounded subset of PAX, not support for every POSIX metadata convention. Live
feed metadata compatibility is not attested by these offline controls.
Where the standard library exposes _fromtarfile, PAX following headers use
its dircheck=False extension protocol. This suppresses legacy AREGTYPE
trailing-slash directory inference from a fallback header name before an
effective local PAX path is applied. Older parsers use their existing public
fromtarfile protocol and retain that legacy inference; this fallback-name edge
is not supported there. These protocols do not establish compatibility with
every future standard-library version or provider metadata convention.
The TAR validator returns immutable AdmittedTarMember records with only
name, type, size, isfile() and isdir(). It no longer returns full
TarInfo objects or extension dictionaries. Both admission and decoding keep
the standard-library member cache empty between streaming reads. No arbitrary
TAR metadata is used to change filesystem ownership, timestamps or permissions.
Windows extraction roots must be a private child below a drive anchor; the drive
anchor itself is refused, including aliases that present a directory as an anchor.
Feed metadata records a SHA-256 digest of both the configured URL and the complete effective redirect target, including parameters and query order, without the fragment (which is not sent to the server). Display URLs redact parameters and queries so authentication tokens do not enter the metadata. Compare the digests when checking whether two downloads used the same target.
Pass the entire parse result when converting a ruleset:
from pathlib import Path
from snort_suricata_rule_converter import RuleParser, convert_rules
parsed = RuleParser().parse_file(Path("input.rules"))
converted = convert_rules(parsed, "suricata", source_dialect="snort3")
if converted.errors:
raise ValueError("Review the diagnostics before using any converted rules")Any parse error rejects the complete conversion, including a valid prefix before
an invalid later record. Parser-produced rules retain their complete parse
diagnostics when their nonempty list is copied or sliced. The ParseResult
also retains diagnostics when a failed parse produces no rules. Empty sequences
have no parse provenance and are refused; use the complete parser-produced
ParseResult for a valid empty input. Manually constructed Rule
objects require allow_detached_rules=True and produce a provenance warning.
The same acknowledgement is required when calling render_rule or
rule_to_dict directly with a manual Rule. Direct rendering, per-rule JSON,
batch conversion and Panorama admission all refuse errors retained from the
complete parse, even if public diagnostics were cleared. An acknowledgement
never overrides a known parse failure or a target compatibility error.
That option acknowledges the caller's responsibility for the original input; it
never overrides a known parse failure or unsafe semantic mapping. Direct option
transformers also refuse constraints they cannot preserve.
File input snapshots record the SHA-256 of the exact consumed bytes, including a UTF-8 BOM and original line endings. JSON, SARIF, conversion comments, Panorama reports/manifests and diff reports retain that digest. On Windows, a retained read handle excludes data writers, writable mappings and deletion during the read. Existing writable handles are refused; close editors that hold one and retry. This does not lock the file after parsing or establish who authored it.
Ruleset record boundaries must use LF or CRLF. Lone CR and other Unicode/control
line separators are refused with a blocking parse diagnostic before comment or
record scanning, including inside quoted text. They are never silently normalized
or accepted through non-strict conversion. Literal escaped rule text such as
\r is unaffected; accepted LF/CRLF text and its raw-byte provenance stay intact.
File admission retains lexical no-follow directory handles or descriptors and
opens the leaf relative to that retained parent. User-selected symlinks, junctions,
parent traversal and writable/untrusted ancestry are refused. Ordinary regular
local files and Windows short-name spellings remain supported. Windows snapshots
require a local drive path; UNC and device namespaces are not supported. Only the
existing root-owned macOS /var and /tmp aliases receive the documented system
alias treatment. No existing ownership or permission repair is performed.
Direct rendering and batch conversion share source-dialect admission: None and
auto infer the dialect; incompatible legacy buffer ambiguity requires an
explicit snort2, snort3, or suricata choice. Unknown strings are refused.
Canonical JSON can describe an ambiguous rule, but does not authorize deploying it.
Option-only transformation helpers require a concrete source dialect.
Fast-pattern chopping is validated within its associated content group. Both the
documented pair-only Snort form and a pair with an optional bare fast_pattern
produce exactly one Suricata fast_pattern:offset,length. The offset is 0-65535,
the length is 1-65535, and the nonempty chop must fit the supported content bytes.
Duplicate markers, orphan pairs, unknown byte escapes and widened-content chops
are refused. See the primary Snort fast-pattern guide
and Suricata prefilter guide.
Raw NUL is refused at public text admission and rule/text output boundaries,
including acknowledged manual rules; quoted hex |00| remains ordinary rule syntax.
Panorama checks normalize equivalent dotted and legacy buffer selectors before applying compatibility policy. Raw HTTP header, host and URI selectors are rejected under either spelling. Supported normalized HTTP buffers remain available; native Panorama validation is still required before deployment.
The 4.0.1 release-readiness tree contains 64 automated tests covering parsing, conversion, reports, Panorama checks, overwrite controls, URL and redirect policy, malicious archives, repository text policy, and deterministic release construction. CI exercises Python 3.12 through 3.14 on Linux, Python 3.12 on Windows and macOS, and both conversion directions against pinned Snort 3.10.0.0 and Suricata 8.0.6 containers.
The larger 4,017-rule Cisco Talos community corpus was validated for version 4.0.0. Snort accepted the full same-dialect round trip with zero warnings. The fail-safe Suricata conversion accepted 3,762 rules, rejected 255 with recorded reasons, and passed native validation with zero errors. Suricata emitted 71 nonfatal duplicate-buffer warnings for source patterns that intentionally revisit an earlier sticky buffer.
Version 4.0.1 changes output-file race handling, feed URL validation, repository controls, and release construction. It does not change conversion semantics. The large third-party corpus has not been rerun for the candidate, so its result is kept explicitly separate from the current hosted native-fixture checks. Full commands, container digests, and scope are recorded in docs/TESTING.md.
| Code | Meaning |
|---|---|
0 |
Operation completed without blocking findings |
1 |
Operational failure, unsafe path, network failure, or invalid invocation |
2 |
Validation findings, conflicts, rejected rules, or an intentional partial result |
130 |
Interrupted by the operator |
- Fool's Gold is an offline cyber-decoy detection and assurance pilot seeking real integration validation.
- Explore the complete Fusion Technology Strategies public portfolio.
python -m pip install -r requirements-dev.txt
python -m ruff format --check .
python -m ruff check .
python -m bandit -q -r snort_suricata_rule_converter.py scripts
python -m pip_audit -r requirements-dev.txt
python -m unittest discover -s tests -v
The production runtime remains one file. Tests, documentation, and repository automation are separate so the executable itself stays portable.
- Rule files are processed locally.
- The tool contains no credentials, tokens, account IDs, or environment-specific resource names.
- Output files are UTF-8 and written atomically.
- Existing outputs are refused unless
--forceis supplied. --forcecannot replace an input file.- Report suspected vulnerabilities through the private process in SECURITY.md.
The converter, tests, and project documentation are licensed under the Apache License 2.0. Third-party rules, vendor documents, and downloaded feeds are not redistributed by this repository and retain their original terms.
Snort is a registered trademark of Cisco. Suricata is a registered trademark of the Open Information Security Foundation. This project is independent and is not endorsed by either organization.
Further PyPI promotion requires a public stable GitHub release whose REST API
reports immutable: true. GitHub locks that release's tag and assets, closing
the tag-mutation window between sequential authorization queries. Existing
mutable releases are rejected rather than silently grandfathered in. Enable
immutable releases before publishing the next fully assembled draft, following
GitHub's immutable release workflow.
The final job checks release identity and stable/public flags again after
approval. GitHub still permits prerelease metadata changes; these flags are
verified snapshots, not an atomic transaction spanning GitHub and PyPI.

