Skip to main content

Enforcing ACL Compliance at Scale with Two Simple NQE Checks

  • December 19, 2025
  • 0 replies
  • 12 views

kuhlskev
Employee

Any time someone tells me they’re manually reviewing ACLs for compliance, I know they’re fighting a losing battle. That approach might work on a small network, but once you’re dealing with thousands—or tens of thousands—of devices, manual validation simply doesn’t scale.

 

This came up recently in a conversation on specific ACL compliance requirement across an environment. They weren’t looking for advanced analytics or a flashy dashboard. They needed a reliable way to ensure their access control lists consistently met two basic security rules, everywhere, all the time.

 

The compliance problem

 

The first requirement was that every deny statement in an ACL must log. From a security and compliance standpoint, a deny that isn’t logged is effectively invisible. Traffic may be blocked, but without a log entry there’s no audit trail and no way to prove enforcement.

 

The second requirement was that every ACL include an explicit deny-all statement. Most platforms apply an implicit deny at the end of an ACL, but that implicit deny typically does not generate logs. If your policy requires denied traffic to be logged, relying on the implicit behavior isn’t sufficient. You need an explicit deny-all so logging is guaranteed.

 

Conceptually, this is simple. Operationally, it becomes painful when you’re managing an environment with roughly tens-of-thousands of network devices. No one wants to validate this by hand, and spot checks don’t satisfy auditors.

 

Using NQE to make the checks continuous

 

I approached this by writing two small, focused NQE queries. Each one answers a single compliance question, and together they cover the full requirement.

 

The first query looks for deny statements that are missing logging. It scans ACL entries and returns only the deny lines that do not include a log action. Instead of reviewing entire ACLs, you immediately get a short list of specific violations that need attention.

/**
* @intent List devices with an ACL that is missing explicit deny with log
* @description Enter your description here (multiple lines are possible)
*/


findDenylog(line) =
foreach child in line.children
where matches(child.text, "deny *ip any any log")
select child;

foreach device in network.devices
where device.platform.os == OS.IOS || device.platform.os == OS.IOS_XE
let appliedacls = ( //finds
foreach line in device.files.config
foreach child in line.children
let match = patternMatch(child.text, `ip access-group {id:string}`)
where isPresent(match)
select match.id
)
let appliedaclsinvty = ( //finds
foreach line in device.files.config
foreach child in line.children
let match = patternMatch(child.text, `access-class {id:string}`)
where isPresent(match)
select match.id
)
let definedacls = (
foreach line in device.files.config
let match = patternMatch(line.text, `access-list {id:string}`)
where isPresent(match)
group match as matches by match.id as id
select id
)
let definedextendedacls = (
foreach line in device.files.config
let match = patternMatch(line.text, `ip access-list extended {id:string}`)
where isPresent(match)
group match as matches by match.id as id
select id
)
let defined = definedacls + definedextendedacls
let applied = appliedacls + appliedaclsinvty
foreach line in device.files.config
let match = patternMatch(line.text, `ip access-list extended {id:string}`)
where isPresent(match)
//only look at ACLs that are applied on ints or vty
where match.id in applied

let findDeny = findDenylog(line)
select {
DeviceName: device.name,
ACL: match.id,
violation: length(findDeny) == 0,
"ACL entries": foreach x in line.children select x.text,
denyline: foreach x in findDeny select x.text
}

The second query checks for ACLs that are missing an explicit deny-all at the end. If an ACL relies solely on the implicit deny, it fails the check. This makes it easy to identify where logging coverage ends prematurely.

/**
* @intent Ensure all ACL Deny entries are configured to log
*/

foreach device in network.devices
let appliedacls = (
foreach line in device.files.config
foreach child in line.children
let match = patternMatch(child.text, `ip access-group {id:string}`)
where isPresent(match)
select match.id
)
let appliedaclsinvty = ( //finds
foreach line in device.files.config
foreach child in line.children
let match = patternMatch(child.text, `access-class {id:string}`)
where isPresent(match)
select match.id
)
let definedextendedacls = (
foreach line in device.files.config
let match = patternMatch(line.text, `ip access-list extended {id:string}`)
where isPresent(match)
foreach child in line.children
where matches(child.text, "*deny*") && !matches(child.text, "*log*")
group match as matches by match.id as id
select id
)
let allappliedacls = appliedacls + appliedaclsinvty
let alldefinedacls = definedextendedacls
foreach acl in alldefinedacls
where acl in allappliedacls
select {
DeviceName: device.name,
ACL: acl
}

Each query is intentionally simple. They’re designed to be easy to understand, easy to explain to auditors, and easy to reuse.

 

Turning checks into something actionable

 

Where this really starts to pay off is when you move beyond running queries and start operationalizing the results. In this case, I pulled both NQE checks into a KPI scorecard along with other compliance rules.

 

At a glance, the dashboard shows overall compliance posture. From there, you can drill into which rule is failing, then into the specific device, ACL, and even the exact line that needs to be fixed. Instead of checking dozens of individual rules manually, you start with a single KPI and let it guide you directly to the problem.

 

 

That’s the difference between running compliance checks and running a compliance program.

 

Where this can go next

 

Once you have this pattern in place, it’s easy to extend. You can merge these checks into a broader ACL compliance query, add rule-ordering validation, flag overly permissive allows, or identify stale rules that no longer serve a purpose. Each new requirement becomes another small NQE check that rolls up into the same scorecard.

 

Final thoughts

 

These checks aren’t complicated, but they automate checking something that must be correct everywhere, all the time, without exception. For large networks, that’s exactly the kind of problem NQE is built to solve.