What a Cisco ASA firewall line looks like
The syslog sample below is fed verbatim into the engine to produce every parser on this page.
<166>%ASA-4-106023: Deny tcp src outside:203.0.113.45/51234 dst inside:192.0.2.10/443 by access-group "outside_access_in"
<164>%ASA-4-106023: Deny udp src outside:198.51.100.23/40012 dst inside:192.0.2.20/53 by access-group "outside_access_in" Detected fields
The engine classified this sample as freeform and consolidated 10 fields across 2 lines. Fields marked literal were identical on every sample line, so they are baked into the pattern as anchors rather than captured.
- literal : literal
- _lit1 : literal · literal
- literal2 : literal
- _lit2 : literal · literal
- literal3 : literal
- _lit3 : literal · literal
- literal4 : literal
- _lit4 : literal · literal
- _lit5 : literal · literal
- quoted_string : quoted_string · literal
Regex (named capture groups)
# sample: <166>%ASA-4-106023: Deny tcp src outside:203.0.113.45/51234 dst inside:192.0.2.10/443 by access-group "outside_access_in"
# groups: literal=<166>%ASA-4-106023, literal2=tcp, literal3=outside:203.0.113.45/51234, literal4=inside:192.0.2.10/443
^(?<literal><\d+>%[A-Za-z]+-\d+-\d+): Deny (?<literal2>[A-Za-z]+) src (?<literal3>[A-Za-z]+:\d+\.\d+\.\d+\.\d+/\d+) dst (?<literal4>[A-Za-z]+:\d+\.\d+\.\d+\.\d+/\d+) by access-group "outside_access_in"$ Grok pattern (Logstash / Elastic)
%{DATA:literal}: Deny %{NOTSPACE:literal2} src %{NOTSPACE:literal3} dst %{NOTSPACE:literal4} by access-group "outside_access_in - note constant field "quoted_string" embedded as literal anchor "outside_access_in" (varying=false)
Wazuh decoder (OS_Regex XML)
<!--
Generated by LogForge - Wazuh decoder (OS_Regex dialect, not PCRE)
sample: <166>%ASA-4-106023: Deny tcp src outside:203.0.113.45/51234 dst inside:192.0.2.10/443 by access-group "outside_access_in"
test with: /var/ossec/bin/wazuh-logtest
-->
<decoder name="cisco-asa-freeform">
<prematch>^</prematch>
</decoder>
<!-- ============================================================
ALERT RULE (starter) — put this in a RULES file, e.g.
/var/ossec/etc/rules/local_rules.xml. Decoders and rules live
in SEPARATE files. The rule matches the decoder above through
<decoded_as>; set <level> and add <field>/<match> conditions so
it alerts only on the events you care about. Rule ids 100000+
are the user range — change them if they collide with yours.
============================================================ -->
<group name="cisco-asa,">
<rule id="100000" level="3">
<decoded_as>cisco-asa-freeform</decoded_as>
<description>cisco-asa: decoded event</description>
</rule>
</group>
- note could not derive a distinctive <prematch>; the emitted anchor is permissive — tune before deploying
- note field "literal" has no safe OS_Regex pattern before the ":" terminator — template truncated; field(s) omitted: literal, _lit1, literal2, _lit2, literal3, _lit3, literal4, _lit4, _lit5, quoted_string
- note no varying fields could be captured — parent decoder emitted alone
- note added a starter alert <rule> (level 3, matched to the decoder via <decoded_as>) — put it in a RULES file (not the decoders file), set the level, and add <field>/<match> conditions; the commented example child rule shows the pattern
- note decoder order and prematch specificity may need site-specific tuning (other decoders in your ruleset can shadow these) — validate with /var/ossec/bin/wazuh-logtest
Wazuh's OS_Regex is not PCRE — a bare . is a literal dot and \. matches any character.
Test Wazuh OS_Regex patterns →
rsyslog template / liblognorm rulebase
version=2
# cisco_asa — liblognorm v2 rulebase (generated by LogForge)
# Usage with rsyslog (mmnormalize runs liblognorm):
# module(load="mmnormalize")
# action(type="mmnormalize" rulebase="/etc/rsyslog.d/cisco_asa.rb" useRawMsg="on")
# Literal "%" is escaped as "%%"; raw tabs are written as \x09.
rule=cisco_asa:%literal:char-to{"extradata":":"}%: Deny %literal2:word% src %literal3:word% dst %literal4:word% by access-group "outside_access_in"
- note trailing literal "\"" reconstructed from line 1
- note chosen parser types: literal=char-to(:), literal2=word, literal3=word, literal4=word
Splunk
# props.conf (search-time extraction)
[<REPLACE_WITH_SOURCETYPE>]
EXTRACT-logforge = (?<literal><\d+>%[A-Za-z]+-\d+-\d+): Deny (?<literal2>[A-Za-z]+) src (?<literal3>[A-Za-z]+:\d+\.\d+\.\d+\.\d+/\d+) dst (?<literal4>[A-Za-z]+:\d+\.\d+\.\d+\.\d+/\d+) by access-group "outside_access_in"
# Quick search-time test in SPL:
# | rex field=_raw "(?<literal><\\d+>%[A-Za-z]+-\\d+-\\d+): Deny (?<literal2>[A-Za-z]+) src (?<literal3>[A-Za-z]+:\\d+\\.\\d+\\.\\d+\\.\\d+/\\d+) dst (?<literal4>[A-Za-z]+:\\d+\\.\\d+\\.\\d+\\.\\d+/\\d+) by access-group \"outside_access_in\"" - note EXTRACT-<class> names must be unique within a sourcetype stanza — rename EXTRACT-logforge if you already use that class for this sourcetype
ES ingest
PUT _ingest/pipeline/cisco-asa
{
"description": "LogForge-generated ingest pipeline for cisco-asa",
"processors": [
{
"grok": {
"field": "message",
"patterns": [
"%{DATA:literal}: Deny %{NOTSPACE:literal2} src %{NOTSPACE:literal3} dst %{NOTSPACE:literal4} by access-group \"outside_access_in"
]
}
}
]
} - note grok: constant field "quoted_string" embedded as literal anchor "outside_access_in" (varying=false)
- note test in Kibana Dev Tools with: POST _ingest/pipeline/cisco-asa/_simulate (supply a docs[] array whose _source.message holds a sample line)
Graylog
# --- Graylog processing pipeline rule (primary) ---
# Paste under System > Pipelines > Manage rules, then attach the rule to a pipeline stage.
rule "cisco-asa-parse"
when
has_field("message")
then
let gp = grok(pattern: "%{DATA:literal}: Deny %{NOTSPACE:literal2} src %{NOTSPACE:literal3} dst %{NOTSPACE:literal4} by access-group \"outside_access_in", value: to_string($message.message), only_named_captures: true);
set_fields(gp);
end
# --- Graylog import-ready extractor JSON (secondary) ---
# Save as a .json file and import under System > Inputs > (input) > Manage extractors > Actions > Import extractors.
{
"extractors": [
{
"title": "cisco-asa",
"extractor_type": "grok",
"converters": [],
"order": 0,
"cursor_strategy": "copy",
"source_field": "message",
"target_field": "",
"extractor_config": {
"grok_pattern": "%{DATA:literal}: Deny %{NOTSPACE:literal2} src %{NOTSPACE:literal3} dst %{NOTSPACE:literal4} by access-group \"outside_access_in",
"named_captures_only": true
},
"condition_type": "none",
"condition_value": ""
}
],
"version": "5.0.0"
} - note grok: constant field "quoted_string" embedded as literal anchor "outside_access_in" (varying=false)
- note primary artifact is the processing-pipeline rule; the extractor JSON is an equivalent import-ready alternative for the classic extractor UI
Datadog
logforge_rule %{notSpace:literal}: Deny %{notSpace:literal2} src %{notSpace:literal3} dst %{notSpace:literal4} by access-group "outside_access_in - note emitted rule name is "logforge_rule"; rename it to match your "cisco-asa" convention if desired
- note constant field "quoted_string" embedded as literal anchor "outside_access_in" (varying=false)
- note paste this line into a Grok Parser processor in a Datadog Log Pipeline; matchers are anchored left-to-right and rule whitespace matches log whitespace. Complex or multi-shape logs may need Helper Rules.
Fluent Bit
[PARSER]
Name cisco-asa
Format regex
Regex ^(?<literal><\d+>%[A-Za-z]+-\d+-\d+): Deny (?<literal2>[A-Za-z]+) src (?<literal3>[A-Za-z]+:\d+\.\d+\.\d+\.\d+/\d+) dst (?<literal4>[A-Za-z]+:\d+\.\d+\.\d+\.\d+/\d+) by access-group "outside_access_in"$
# Time_Key <name of a timestamp capture group, if any>
# Time_Format <strptime format, e.g. %Y-%m-%dT%H:%M:%S>
# Fluentd <parse> block:
# <parse>
# @type regexp
# expression /^(?<literal><\d+>%[A-Za-z]+-\d+-\d+): Deny (?<literal2>[A-Za-z]+) src (?<literal3>[A-Za-z]+:\d+\.\d+\.\d+\.\d+\/\d+) dst (?<literal4>[A-Za-z]+:\d+\.\d+\.\d+\.\d+\/\d+) by access-group "outside_access_in"$/
# </parse>
Vector
[transforms.cisco_asa_parse]
type = "remap"
inputs = ["REPLACE_WITH_SOURCE"]
source = '''
. |= parse_regex!(.message, r'(?P<literal><\d+>%[A-Za-z]+-\d+-\d+): Deny (?P<literal2>[A-Za-z]+) src (?P<literal3>[A-Za-z]+:\d+\.\d+\.\d+\.\d+/\d+) dst (?P<literal4>[A-Za-z]+:\d+\.\d+\.\d+\.\d+/\d+) by access-group "outside_access_in"')
''' Loki
# promtail pipeline for "cisco-asa" (generated by LogForge)
# Add these stages under a scrape_config in your promtail config:
# scrape_configs:
# - job_name: cisco-asa
# pipeline_stages:
# (the stages below are indented to sit under pipeline_stages)
pipeline_stages:
- regex:
expression: '^(?P<literal><\d+>%[A-Za-z]+-\d+-\d+): Deny (?P<literal2>[A-Za-z]+) src (?P<literal3>[A-Za-z]+:\d+\.\d+\.\d+\.\d+/\d+) dst (?P<literal4>[A-Za-z]+:\d+\.\d+\.\d+\.\d+/\d+) by access-group "outside_access_in"$'
- note no low-cardinality field found to promote to a Loki label — omitted the `- labels:` stage; every captured field stays in the extracted map for later stages
- note left in the extracted map (NOT promoted to labels — high cardinality would explode Loki streams): literal, literal2, literal3, literal4
syslog-ng
parser p_cisco_asa {
regexp-parser(
prefix(".cisco_asa.")
patterns("(?<literal><\\d+>%[A-Za-z]+-\\d+-\\d+): Deny (?<literal2>[A-Za-z]+) src (?<literal3>[A-Za-z]+:\\d+\\.\\d+\\.\\d+\\.\\d+/\\d+) dst (?<literal4>[A-Za-z]+:\\d+\\.\\d+\\.\\d+\\.\\d+/\\d+) by access-group \"outside_access_in\"")
);
}; - note captured fields are stored as name-value pairs under the prefix ".cisco_asa." (e.g. a group (?<srcip>…) becomes ".cisco_asa.srcip")
FAQ
- What is the %ASA-x-nnnnnn tag in a Cisco ASA log?
- It is the ASA message identifier. %ASA is the device class, the single digit after it is the syslog severity (0 emergency … 7 debug), and the six-digit number is the message ID that names the specific event — 106023 for an ACL deny, 302013 for a TCP connection built, and so on. The message ID, not the free text, is what you branch on when parsing.
- Why can I not write one regex for all ASA messages?
- Because the text after the mnemonic is free-form and its structure differs per message ID. A 106023 deny, a 302013 connection-built, and a 106100 ACL-hit line arrange their fields differently. Extract the severity and message ID with a common pattern, then apply a per-message-id sub-pattern to the body — order-independent field templates keyed on the ID.
- How are addresses formatted in ASA logs?
- As interface:ip/port. The ASA prepends the interface name (outside, inside, dmz) to the address with a colon and appends the port after a slash — e.g. outside:203.0.113.45/51234. For NAT/xlate messages a translated address often follows in parentheses. This means you get the traffic direction from the interface name without a separate field.
- Which message IDs matter most for security monitoring?
- 106023 (deny by access-group) and 106100 (ACL permit/deny with hit counts) for policy enforcement, 302013/302014 (TCP built/teardown) and 302015/302016 (UDP) for connection tracking, and 305011/305012 for NAT/xlate. Alerting on 106023 grouped by source IP is the classic scan- and blocked-traffic detection.
Try it on your own Cisco ASA firewall lines
Paste a few real lines, review the detected fields, and copy whichever format your stack needs. Free, no account, nothing uploaded.
Open this sample in LogForge →