jq for Infrastructure APIs
Infrastructure APIs return JSON. Master these patterns to automate ISE, pfSense, Vault, and wireless controllers.
Cisco ISE via netapi
Session Analysis
# Active sessions table
netapi ise --format json mnt sessions | jq -r '
["MAC", "USER", "IP", "STATE"],
(.[] | [.calling_station_id, .user_name, .framed_ip_address, .session_state]) |
@tsv
' | column -t
# Sessions with timing
netapi ise --format json mnt sessions | jq '
.[] | select(.session_state == "AUTHENTICATED") |
{
mac: .calling_station_id,
user: .user_name,
ip: .framed_ip_address,
duration: (.acct_session_time | tonumber | . / 60 | floor | "\(.)m"),
policy: .access_policy_matched_rule
}
'
# Sessions by authorization profile
netapi ise --format json mnt sessions | jq '
group_by(.authorization_profile) |
map({profile: .[0].authorization_profile, count: length}) |
sort_by(.count) | reverse
'
# Find sessions for specific MAC
netapi ise --format json mnt sessions | jq --arg mac "AA:BB:CC:DD:EE:FF" '
.[] | select(.calling_station_id | ascii_upcase == ($mac | ascii_upcase))
'
Policy Sets
# List policy sets
netapi ise --format json get-policy-sets | jq '.[] | {
name,
state,
rank,
rules: (.rules | length)
}'
# Find disabled policies
netapi ise --format json get-policy-sets | jq '
.[] | select(.state == "disabled") | .name
'
# Policy set rules detail
netapi ise --format json get-policy-sets | jq '
.[] | select(.name == "Wired_802.1X_Closed") |
.rules[] | {name, state, condition, authzProfile}
'
# Find which policy sets use specific authorization profile
PROFILE="Corp-Access"
netapi ise --format json get-policy-sets | jq --arg p "$PROFILE" '
.[] | select(.rules[]?.authzProfile == $p) |
{
policySet: .name,
matchingRules: [.rules[] | select(.authzProfile == $p) | .name]
}
'
Authorization Profiles
# List profiles with dACL
netapi ise --format json get-authz-profiles | jq '
.[] | {name, dacl: .daclName, vlan: .vlan}
'
# Profiles using specific dACL
netapi ise --format json get-authz-profiles | jq --arg dacl "PERMIT_ALL" '
.[] | select(.daclName == $dacl) | .name
'
dACL Operations
# Extract dACL rules
netapi ise api-call openapi GET '/api/v1/policy/network-access/downloadable-acl' | jq -r '
.response[] | select(.name | test("Linux")) |
"=== \(.name) ===\n\(.dacl)"
' | sed 's/\\n/\n/g'
# Compare two dACLs
DACL1="Linux-Onboard"
DACL2="Linux-EAP-TLS"
diff -u \
<(netapi ise api-call openapi GET '/api/v1/policy/network-access/downloadable-acl' | \
jq -r --arg d "$DACL1" '.response[] | select(.name == $d) | .dacl' | tr '\\n' '\n') \
<(netapi ise api-call openapi GET '/api/v1/policy/network-access/downloadable-acl' | \
jq -r --arg d "$DACL2" '.response[] | select(.name == $d) | .dacl' | tr '\\n' '\n')
Endpoint Identity Groups
# List groups with descriptions
netapi ise api-call openapi GET '/api/v1/endpoint-identity-group?size=100' | jq -r '
["GROUP", "DESCRIPTION", "SYSTEM"],
(.response[] | [.name, (.description // "N/A"), (if .systemDefined then "yes" else "no" end)]) |
@tsv
' | column -t -s$'\t'
# Find endpoints in group
netapi ise api-call openapi GET '/api/v1/endpoint?filter=groupId.EQ.<group-id>' | jq '
.response[] | {mac, profile: .profileId}
'
Certificate Trust Store
# Certificates expiring within 90 days
netapi ise api-call openapi GET '/api/v1/certs/trusted-certificate?size=100' | jq '
(now + 7776000) as $cutoff |
.response[] |
(.expirationDate | split("T")[0] | strptime("%Y-%m-%d") | mktime) as $exp |
select($exp < $cutoff) |
{
name: .friendlyName,
expires: .expirationDate | split("T")[0],
issuer: .issuedBy
}
'
pfSense via netapi
DNS Overrides
# List all DNS overrides
netapi pfsense dns list --format json | jq '.[] | {host, domain, ip}'
# Find entries matching pattern
netapi pfsense dns list --format json | jq '
.[] | select(.host | test("vault"))
'
# Count by domain
netapi pfsense dns list --format json | jq '
group_by(.domain) |
map({domain: .[0].domain, count: length}) |
sort_by(.count) | reverse
'
# Find duplicates (same host, different IPs)
netapi pfsense dns list --format json | jq '
group_by(.host) |
map(select(length > 1)) |
.[] | {host: .[0].host, ips: [.[] | .ip]}
'
Firewall Rules
# List rules summary
netapi pfsense rules list --format json | jq -r '
["IFACE", "TYPE", "SRC", "DST", "DESC"],
(.[] | [.interface, .type, .source, .destination, .descr]) |
@tsv
' | column -t
# Rules with logging disabled (potential gap)
netapi pfsense rules list --format json | jq '
.[] | select(.log == false) |
{interface, type, source, destination, descr}
'
# Count rules per interface
netapi pfsense rules list --format json | jq '
group_by(.interface) |
map({interface: .[0].interface, count: length}) |
sort_by(.count) | reverse
'
DHCP Leases
# Active leases
netapi pfsense dhcp leases --format json | jq '.[] | {
ip,
mac,
hostname,
starts: .starts,
ends: .ends
}'
# Leases expiring within 1 hour
netapi pfsense dhcp leases --format json | jq '
(now + 3600) as $soon |
.[] | select(.ends | strptime("%Y-%m-%d %H:%M:%S") | mktime < $soon) |
{ip, mac, hostname, ends}
'
HashiCorp Vault
Secret Listing
# List keys in path
vault kv list -format=json secret/domus | jq -r '.[]'
# Recursive list (KV v2)
vault kv list -format=json secret/data | jq -r '.[]'
# Keys with metadata
vault kv metadata list -format=json secret/domus | jq '
to_entries[] | {key: .key, versions: .value.versions}
'
PKI Operations
# Issue certificate and extract parts
vault write -format=json pki_int/issue/domus-client \
common_name="host.inside.domusdigitalis.dev" | jq -r '
{
certificate: .data.certificate,
private_key: .data.private_key,
ca_chain: .data.ca_chain[0],
expiration: (.data.expiration | strftime("%Y-%m-%d %H:%M:%S")),
serial: .data.serial_number
}
'
# Just the cert
vault write -format=json pki_int/issue/domus-client \
common_name="host.example.com" | jq -r '.data.certificate'
# Certificate inventory
vault list -format=json pki_int/certs | jq -r '.[]' | while read serial; do
vault read -format=json "pki_int/cert/$serial" | jq -r '
[.data.serial_number, (.data.expiration | strftime("%Y-%m-%d")), .data.common_name] | @tsv
'
done | column -t -N "SERIAL,EXPIRES,CN"
# Certificates expiring within 30 days
vault list -format=json pki_int/certs | jq -r '.[]' | while read serial; do
vault read -format=json "pki_int/cert/$serial" 2>/dev/null
done | jq -s '
(now + 2592000) as $cutoff |
.[] | select(.data.expiration < $cutoff) |
{cn: .data.common_name, expires: (.data.expiration | strftime("%Y-%m-%d")), serial: .data.serial_number}
'
Token and Auth Info
# Current token info
vault token lookup -format=json | jq '{
display_name,
policies,
ttl,
creation_time: (.creation_time | strftime("%Y-%m-%d %H:%M:%S"))
}'
# List auth methods
vault auth list -format=json | jq 'to_entries[] | {
path: .key,
type: .value.type,
description: .value.description
}'
SSH Secrets Engine
# Sign SSH public key
vault write -format=json ssh-client-signer/sign/admin \
public_key=@~/.ssh/id_ed25519.pub | jq -r '.data.signed_key'
# With custom principals
vault write -format=json ssh-client-signer/sign/admin \
public_key=@~/.ssh/id_ed25519.pub \
valid_principals="root,admin" | jq '{
signed_key: .data.signed_key,
serial: .data.serial_number,
valid_until: (.data.valid_after | . + 86400 | strftime("%Y-%m-%d %H:%M:%S"))
}'
Wireless Controllers (WLC)
Client Distribution
# Clients per WLAN
netapi wlc clients --format json | jq '
group_by(.wlan_profile_name) |
map({wlan: .[0].wlan_profile_name, clients: length}) |
sort_by(.clients) | reverse
'
# AP load distribution
netapi wlc clients --format json | jq '
group_by(.ap_name) |
map({
ap: .[0].ap_name,
clients: length,
channels: ([.[] | .channel] | unique)
}) |
sort_by(.clients) | reverse
'
# Clients with weak signal
netapi wlc clients --format json | jq '
.[] | select((.rssi | tonumber) < -75) |
{mac: .mac_address, ap: .ap_name, rssi, wlan: .wlan_profile_name}
'
Rogue Detection
# Nearby rogues (strong signal = close)
netapi wlc rogues --format json | jq '
.[] | select((.rssi | tonumber) > -70) |
{
mac: .mac_address,
rssi,
ssid,
detecting_ap: .detecting_aps[0],
classification: .classification
}
'
# Rogue count by classification
netapi wlc rogues --format json | jq '
group_by(.classification) |
map({classification: .[0].classification, count: length})
'
AP Status
# AP inventory
netapi wlc aps --format json | jq '.[] | {
name: .ap_name,
model,
ip: .ip_address,
status: .operational_status,
clients: .client_count
}'
# APs with issues
netapi wlc aps --format json | jq '
.[] | select(.operational_status != "associated") |
{name: .ap_name, status: .operational_status, last_reboot: .reboot_reason}
'
Generic API Patterns
Explore Unknown API Response
# Step 1: What type is the root?
curl -s "$API_URL" | jq 'type'
# Step 2: What keys exist?
curl -s "$API_URL" | jq 'keys'
# Step 3: Keys with types
curl -s "$API_URL" | jq 'to_entries[] | "\(.key): \(.value | type)"'
# Step 4: Drill into nested objects
curl -s "$API_URL" | jq '.data | to_entries[] | "\(.key): \(.value | type)"'
Pagination Handling
# Collect all pages
PAGE=1
while true; do
RESPONSE=$(curl -s "$API_URL?page=$PAGE&per_page=100")
COUNT=$(echo "$RESPONSE" | jq '.items | length')
echo "$RESPONSE" | jq '.items[]'
[[ "$COUNT" -lt 100 ]] && break
((PAGE++))
done | jq -s '.'
Error Handling
# Check for error field
curl -s "$API_URL" | jq '
if .error then
"ERROR: \(.error.message)"
else
.data
end
'
# Exit on error
RESPONSE=$(curl -s "$API_URL")
if echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
echo "API Error: $(echo "$RESPONSE" | jq -r '.error.message')" >&2
exit 1
fi
echo "$RESPONSE" | jq '.data'
Wazuh SIEM
Alert Triage
# High severity alerts from last hour
curl -s -k -u "$WAZUH_USER:$WAZUH_PASS" \
"https://${WAZUH_API}/security/events?limit=1000" | jq '
.data.affected_items[] |
select(.rule.level >= 12) |
{
timestamp,
agent: .agent.name,
rule_id: .rule.id,
description: .rule.description,
src_ip: .data.srcip,
dst_ip: .data.dstip
}
'
# Alerts grouped by rule
curl -s -k -u "$WAZUH_USER:$WAZUH_PASS" \
"https://${WAZUH_API}/security/events?limit=5000" | jq '
[.data.affected_items[] | {rule: .rule.id, description: .rule.description}] |
group_by(.rule) |
map({rule: .[0].rule, description: .[0].description, count: length}) |
sort_by(.count) | reverse | .[0:20]
'
# MITRE ATT&CK tactic breakdown
curl -s -k -u "$WAZUH_USER:$WAZUH_PASS" \
"https://${WAZUH_API}/security/events?limit=5000" | jq '
[.data.affected_items[] | select(.rule.mitre) | .rule.mitre.tactic[]] |
group_by(.) | map({tactic: .[0], count: length}) | sort_by(.count) | reverse
'
# Top source IPs generating alerts
curl -s -k -u "$WAZUH_USER:$WAZUH_PASS" \
"https://${WAZUH_API}/security/events?limit=5000" | jq '
[.data.affected_items[] | select(.data.srcip) | .data.srcip] |
group_by(.) | map({ip: .[0], count: length}) |
sort_by(.count) | reverse | .[0:15]
'
Agent Health
# Agents by status
curl -s -k -u "$WAZUH_USER:$WAZUH_PASS" \
"https://${WAZUH_API}/agents" | jq '
[.data.affected_items[] | .status] |
group_by(.) | map({status: .[0], count: length})
'
# Disconnected agents (offline > 1 hour)
curl -s -k -u "$WAZUH_USER:$WAZUH_PASS" \
"https://${WAZUH_API}/agents" | jq '
.data.affected_items[] |
select(.status == "disconnected") |
{
name,
ip,
last_keepalive: .lastKeepAlive,
os: .os.platform,
version
}
'
# Agent version distribution
curl -s -k -u "$WAZUH_USER:$WAZUH_PASS" \
"https://${WAZUH_API}/agents" | jq '
[.data.affected_items[] | .version // "unknown"] |
group_by(.) | map({version: .[0], count: length}) |
sort_by(.count) | reverse
'
Vulnerability Assessment
# Critical CVEs across all agents
curl -s -k -u "$WAZUH_USER:$WAZUH_PASS" \
"https://${WAZUH_API}/vulnerability?limit=5000" | jq '
.data.affected_items[] |
select(.severity == "Critical") |
{
agent: .agent_id,
cve: .cve,
package: .package.name,
version: .package.version,
cvss: .cvss3_score
}
'
# Vulnerability count by severity
curl -s -k -u "$WAZUH_USER:$WAZUH_PASS" \
"https://${WAZUH_API}/vulnerability?limit=5000" | jq '
[.data.affected_items[] | .severity] |
group_by(.) | map({severity: .[0], count: length})
'
# Most common vulnerable packages
curl -s -k -u "$WAZUH_USER:$WAZUH_PASS" \
"https://${WAZUH_API}/vulnerability?limit=5000" | jq '
[.data.affected_items[] | .package.name] |
group_by(.) | map({package: .[0], count: length}) |
sort_by(.count) | reverse | .[0:10]
'
gopass Secrets Inventory
Audit secrets structure without exposing values.
List Structure
# List all secret paths (tree structure to JSON)
gopass ls --flat | jq -R -s 'split("\n") | map(select(length > 0))'
# Group by first path component (category)
gopass ls --flat | jq -R -s '
split("\n") | map(select(length > 0)) |
map(split("/")[0]) |
group_by(.) | map({category: .[0], count: length}) |
sort_by(.count) | reverse
'
# Find secrets by pattern (without revealing content)
gopass ls --flat | jq -R -s '
split("\n") | map(select(. | test("ise|radius|wifi"; "i")))
'
Audit Metadata
# Extract age recipients from .age-recipients files
find ~/.local/share/gopass/stores -name ".age-recipients" -exec cat {} \; | jq -R -s '
split("\n") | map(select(length > 0)) | unique |
{total_keys: length, keys: .}
'
# Secret age (modification times)
gopass ls --flat | while read path; do
MTIME=$(gopass show -n "$path" 2>/dev/null | head -1 | stat -c %Y 2>/dev/null)
echo "{\"path\":\"$path\",\"modified\":$MTIME}"
done | jq -s 'sort_by(.modified)'
# Find potentially stale secrets (not modified in 1 year)
CUTOFF=$(($(date +%s) - 31536000))
gopass ls --flat | while read path; do
stat ~/.local/share/gopass/stores/*/$(dirname "$path")/.gpg-id 2>/dev/null
done
ISE Data Connect (MSCHAPv2 Migration)
Query ISE Data Connect for migration analysis.
Find MSCHAPv2 Authentications
# Export from ISE Data Connect to JSON, then analyze
# MSCHAPv2 vs EAP-TLS breakdown
cat ise_radius_auth.json | jq '
[.[] | .eap_authentication] |
group_by(.) | map({method: .[0], count: length}) |
sort_by(.count) | reverse
'
# Devices still using MSCHAPv2
cat ise_radius_auth.json | jq '
.[] | select(.eap_authentication | test("MSCHAPv2|PEAP")) |
{
mac: .calling_station_id,
user: .user_name,
device_type: .device_type,
os: .operating_system,
policy: .authorization_policy_matched_rule
}
' | jq -s 'unique_by(.mac)'
# Count by device type (prioritize migration order)
cat ise_radius_auth.json | jq '
[.[] | select(.eap_authentication | test("MSCHAPv2|PEAP")) | .device_type // "Unknown"] |
group_by(.) | map({type: .[0], count: length}) |
sort_by(.count) | reverse
'
# Devices authenticating with MSCHAPv2 in last 30 days (active)
cat ise_radius_auth.json | jq --arg cutoff "$(date -d '30 days ago' +%Y-%m-%d)" '
.[] |
select(.eap_authentication | test("MSCHAPv2|PEAP")) |
select(.timestamp[:10] >= $cutoff) |
.calling_station_id
' | jq -s 'unique | length'
Migration Progress Tracking
# Compare two exports (before/after migration window)
jq -n --slurpfile before before.json --slurpfile after after.json '
($before[0] | [.[] | select(.eap_authentication | test("MSCHAPv2")) | .calling_station_id] | unique) as $before_macs |
($after[0] | [.[] | select(.eap_authentication | test("MSCHAPv2")) | .calling_station_id] | unique) as $after_macs |
{
before_count: ($before_macs | length),
after_count: ($after_macs | length),
migrated: (($before_macs - $after_macs) | length),
new_mschapv2: (($after_macs - $before_macs) | length),
still_mschapv2: $after_macs
}
'
# Successful EAP-TLS authentications (migration targets confirmed)
cat ise_radius_auth.json | jq '
.[] | select(.eap_authentication == "EAP-TLS") |
select(.passed == true) |
{mac: .calling_station_id, user: .user_name, cert_cn: .certificate_cn}
' | jq -s 'unique_by(.mac) | length'
Azure/Sentinel Integration
Process KQL query results exported as JSON.
Incident Analysis
# az sentinel incident list (via CLI)
az sentinel incident list --resource-group $RG --workspace-name $WORKSPACE -o json | jq '
.[] | select(.properties.severity == "High") |
{
title: .properties.title,
severity: .properties.severity,
status: .properties.status,
created: .properties.createdTimeUtc,
tactics: .properties.additionalData.tactics
}
'
# Group incidents by classification
az sentinel incident list --resource-group $RG --workspace-name $WORKSPACE -o json | jq '
[.[] | .properties.classification // "Undetermined"] |
group_by(.) | map({classification: .[0], count: length})
'
# Time to close (closed incidents)
az sentinel incident list --resource-group $RG --workspace-name $WORKSPACE -o json | jq '
.[] | select(.properties.status == "Closed") |
((.properties.closedTimeUtc | fromdateiso8601) - (.properties.createdTimeUtc | fromdateiso8601)) as $duration |
{
title: .properties.title,
hours_to_close: ($duration / 3600 | floor)
}
'
KQL Export Processing
# Process exported KQL query results
# Example: SigninLogs query results
cat signinlogs_export.json | jq '
.[] | select(.ResultType != 0) | # Failed logins
{
user: .UserPrincipalName,
app: .AppDisplayName,
ip: .IPAddress,
location: .Location,
error: .ResultDescription,
time: .TimeGenerated
}
'
# Failed logins by user
cat signinlogs_export.json | jq '
[.[] | select(.ResultType != 0) | .UserPrincipalName] |
group_by(.) | map({user: .[0], failures: length}) |
sort_by(.failures) | reverse | .[0:10]
'
# Geographic anomalies (logins from new locations)
cat signinlogs_export.json | jq '
[.[] | {user: .UserPrincipalName, country: .Location.countryOrRegion}] |
group_by(.user) | map({
user: .[0].user,
countries: [.[].country] | unique,
country_count: ([.[].country] | unique | length)
}) | map(select(.country_count > 2))
'
Defender Alerts
# Microsoft Defender alerts via Graph API
az rest --method get \
--uri "https://graph.microsoft.com/v1.0/security/alerts_v2" | jq '
.value[] | select(.severity == "high") |
{
title,
severity,
status,
category,
devices: [.evidence[] | select(.type == "device") | .deviceDnsName],
created: .createdDateTime
}
'
# Alert timeline (for incident correlation)
az rest --method get \
--uri "https://graph.microsoft.com/v1.0/security/alerts_v2" | jq '
[.value[] | {
time: .createdDateTime,
title,
severity,
category
}] | sort_by(.time)
'
Certificate Expiry Monitoring
Comprehensive PKI monitoring across infrastructure.
ISE Trust Store
# All certificates expiring in 90 days
netapi ise api-call openapi GET '/api/v1/certs/trusted-certificate?size=200' | jq '
(now + 7776000) as $cutoff |
.response[] |
(.expirationDate | fromdateiso8601) as $exp |
select($exp < $cutoff) |
{
name: .friendlyName,
subject: .subject,
issuer: .issuedBy,
expires: (.expirationDate | split("T")[0]),
days_left: ((($exp - now) / 86400) | floor),
usage: [.usedBy[]?] | join(", ")
}
' | jq -s 'sort_by(.days_left)'
# System certificates (identity certs)
netapi ise api-call openapi GET '/api/v1/certs/system-certificate' | jq '
.response[] |
{
host: .hostName,
cn: .subject | capture("CN=(?<cn>[^,]+)").cn,
expires: .expirationDate | split("T")[0],
usedFor: .usedFor
}
'
Vault PKI Comprehensive
# All certs with expiry timeline
vault list -format=json pki_int/certs 2>/dev/null | jq -r '.[]' | while read serial; do
vault read -format=json "pki_int/cert/$serial" 2>/dev/null
done | jq -s '
. as $certs |
now as $now |
$certs | map(
(.data.expiration - $now) / 86400 | floor
) as $days |
{
total: ($certs | length),
expired: ([$certs[] | select(.data.expiration < now)] | length),
expiring_30d: ([$certs[] | select(.data.expiration < (now + 2592000) and .data.expiration > now)] | length),
expiring_90d: ([$certs[] | select(.data.expiration < (now + 7776000) and .data.expiration > now)] | length)
}
'
Host Certificate Check
# Check multiple hosts via openssl, output as JSON
HOSTS="ise-01.domain:443 vault.domain:8200 ldaps.domain:636"
for hostport in $HOSTS; do
EXPIRY=$(echo | openssl s_client -connect "$hostport" -servername "${hostport%%:*}" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
echo "{\"host\":\"$hostport\",\"expires\":\"$EXPIRY\"}"
done | jq -s '.'
Related
-
Kubernetes - kubectl patterns