เอกสารฉบับเต็มสำหรับ SOC engineer · 15 detections paste-able · onboarding sprint 4 สัปดาห์ · macros + lookups + dashboard XML outline · ทุก SPL ทดสอบ syntax แต่ ต้อง tune ที่หน้างาน เพราะ field name อาจแตกต่างตาม Splunk TA version
ถ้า prerequisite ไม่ครบ · alert จะท่วม false positive และทีมจะเลิกเชื่อ detection
| Source | Splunk index | Sourcetype | ใช้ใน detection |
|---|---|---|---|
| Zeek conn + ssl | zeek | bro:conn / bro:ssl | D1, D2, D3, D4 |
| NGFW (Palo Alto) | pan_logs | pan:traffic | D1, D2 |
| NGFW (Fortinet) | fortinet | fortigate_traffic | D1, D2 |
| DNS BIND / Infoblox | dns | infoblox:dns / isc:bind:query | D7 |
| Cisco Umbrella DNS | cisco_umbrella | cisco_umbrella:dns | D7 |
| CrowdStrike Falcon | crowdstrike | crowdstrike:event:json | D5, D6, D8 |
| SentinelOne | sentinelone | sentinelone:dv | D5, D6, D8 (alt) |
| Zscaler NSS | zscaler | zscalernss-web | D9, D10 |
| Netskope | netskope | netskope:webtxn / netskope:application:json | D9, D10, D11 |
| AWS CloudTrail | aws | aws:cloudtrail | D12 |
| Azure Key Vault | azure | mscs:azure:eventhub | D12 |
| sslyze JSON (HEC) | cert_inventory | sslyze:scan | D13, D14, D15 |
earliest=-30d นี้สร้าง stats summaryasset_cmdb.csv mapping ip,hostname,business_unit,criticality,ownerbusiness_partner_asn.csv, known_crypto_consumers.csv, authorized_key_readers.csvทุกตัวเป็น critical · ส่งเข้า SOC analyst tier 1 ทันที · response SLA 15 นาที
Network_Traffic populated · Zeek conn.log หรือ NGFW traffic log| tstats summariesonly=t sum(All_Traffic.bytes_out) as bytes_out
from datamodel=Network_Traffic.All_Traffic
where All_Traffic.app=ssl OR All_Traffic.dest_port IN (443,8443)
AND NOT All_Traffic.dest_category=internal
by All_Traffic.src, _time span=1h
| eventstats avg(bytes_out) as avg_30d, stdev(bytes_out) as sd_30d
by All_Traffic.src
| eval z_score = (bytes_out - avg_30d) / sd_30d
| where z_score > 4 AND bytes_out > 500000000
| eval gb_out = round(bytes_out/1024/1024/1024, 2)
| lookup asset_cmdb.csv ip as "All_Traffic.src" OUTPUT business_unit, criticality, owner
| table _time, All_Traffic.src, hostname, business_unit, gb_out, z_score, avg_30d
| sort -z_score
index=zeek bro:conn · lookup business_partner_asn.csvindex=zeek sourcetype=bro:conn proto=tcp service=ssl | where duration > 14400 | iplocation id.resp_h | lookup business_partner_asn.csv asn OUTPUT partner as is_partner | where isnull(is_partner) AND Country!="Thailand" | eval gb = round(orig_bytes/1024/1024/1024, 2) | where gb > 1 | lookup asset_cmdb.csv ip as "id.orig_h" OUTPUT business_unit, criticality | table _time, id.orig_h, business_unit, id.resp_h, Country, asn, duration, gb, ja3
business_partner_asn.csv ที่ AWS/Azure/GCP/Cloudflare ASN + ลูกค้า/partner cloud providers · false positive ที่พบ: SaaS dashboards ที่เปิดทิ้งไว้, monitoring agents (Datadog, NewRelic)7z -p, winrar a -p, openssl enc, gpg --encrypt) แล้วตามด้วย TLS upload > 100MB จาก host เดียวกันภายใน 30 นาที · นี่เป็น Multi-stage correlationProcessRollup2 + Zeek conn.log · join บน hostindex=crowdstrike sourcetype=crowdstrike:event:json
event_simpleName=ProcessRollup2
(CommandLine="*7z*-p*" OR CommandLine="*openssl enc*"
OR CommandLine="*gpg*--encrypt*" OR CommandLine="*rar* a -p*"
OR CommandLine="*Compress-Archive*Password*")
| eval staging_time=_time, host=ComputerName
| table host, UserName, ParentBaseFileName, FileName, CommandLine, staging_time
| join host [
search index=zeek sourcetype=bro:conn service=ssl earliest=-30m
| where orig_bytes > 100000000
| stats min(_time) as upload_time, sum(orig_bytes) as bytes
by id.orig_h as host
]
| where upload_time - staging_time BETWEEN 0 AND 1800
| eval delta_min = round((upload_time - staging_time)/60, 1)
| eval gb = round(bytes/1073741824, 2)
| table host, UserName, CommandLine, delta_min, gb
.pem, .pfx, id_rsa, *.key, ~/.ssh/* ที่ไม่ใช่ authorized service · ขโมยกุญแจวันนี้ → decrypt ข้อมูลเก่าได้ในอนาคต — ตรงกับ HNDL philosophyFileOpenInfo หรือ SentinelOne file access · lookup authorized_key_readers.csvindex=crowdstrike event_simpleName=FileOpenInfo (TargetFileName="*.pem" OR TargetFileName="*.pfx" OR TargetFileName="*\\.ssh\\id_*" OR TargetFileName="*private.key" OR TargetFileName="*\\AppData\\Roaming\\*\\Local State" OR TargetFileName="/root/.ssh/*" OR TargetFileName="/home/*/.ssh/*") | stats count, values(TargetFileName) as files, values(UserName) as users by ComputerName as host, ImageFileName | lookup authorized_key_readers.csv process as ImageFileName OUTPUT is_authorized | where isnull(is_authorized) | sort -count
High value detection · response SLA 1 ชม. · trial period 2 สัปดาห์ก่อน promote เป็น production alert
abuse_ja3.csv)index=zeek sourcetype=bro:ssl
| stats count, dc(id.orig_h) as host_count,
values(server_name) as sni,
latest(_time) as last_seen by ja3
| lookup abuse_ja3.csv ja3 OUTPUT malware_family, threat_type
| where host_count <= 2 AND count > 5
| eval suspicious_score = case(
isnotnull(malware_family), "critical",
match(mvjoin(sni,","), "(?i)(pastebin|telegram|discord|onion)"), "high",
count < 20, "medium",
1=1, "low")
| eval last_seen = strftime(last_seen, "%Y-%m-%d %H:%M")
| where suspicious_score!="low"
| sort -suspicious_score, count
index=dns sourcetype IN (infoblox:dns,cisco_umbrella:dns,isc:bind:query)
| eval q_len = len(query)
| eval subdomain = mvindex(split(query,"."),0)
| eval entropy = round(, 2)
| stats count, avg(q_len) as avg_len, max(entropy) as max_ent,
dc(query) as unique_queries,
values(reply_code) as codes
by src_ip, query_domain
| where avg_len > 30 AND max_ent > 4.0 AND unique_queries > 50
| eval nxdomain_burst = if(match(mvjoin(codes,","),"NXDOMAIN"), "yes", "no")
| lookup asset_cmdb.csv ip as src_ip OUTPUT business_unit, criticality
| sort -unique_queries
shannon_entropy macro (ดู section 06) · whitelist CDN, anti-malware update domains · false positive จาก telemetry domains (segment.io, google-analytics)index=zscaler sourcetype=zscalernss-web action=allow
| eval gb_up = bytes_out/1073741824
| stats sum(gb_up) as gb_total, count, values(url) as urls,
values(appname) as apps
by user, urlcategory, bin(_time, 1d) as day
| where (urlcategory IN ("personal_storage","unsanctioned_apps","shadow_it")
OR risk_score > 70) AND gb_total > 2
| eval severity = case(gb_total > 20, "critical",
gb_total > 10, "high", 1=1, "medium")
| sort -gb_total
index=netskope sourcetype="netskope:webtxn" activity=Upload OR activity=PostUpload | eval gb_up = client_bytes/1073741824 | stats sum(gb_up) as gb_total by user, app, app_risk, bin(_time, 1d) as day | where (app_risk >= 7 OR match(app_category,"(?i)Personal")) AND gb_total > 2 | sort -gb_total
index=aws sourcetype=aws:cloudtrail
eventSource=kms.amazonaws.com
eventName IN (Decrypt,GenerateDataKey)
| bin _time span=1h
| stats count by _time, userIdentity.userName, "resources{}.ARN"
| eventstats avg(count) as avg_30d, stdev(count) as sd_30d
by userIdentity.userName, "resources{}.ARN"
| eval z_score = (count - avg_30d) / sd_30d
| where z_score > 5 AND count > 1000
| table _time, userIdentity.userName, "resources{}.ARN", count, avg_30d, z_score
Daily report ส่ง security team · ไม่ต้อง real-time · ใช้ trend analysis เป็นหลัก
index=zeek sourcetype=bro:ssl earliest=-1d@d latest=@d
| eval issue = case(
match(cipher, "(?i)RC4|DES|MD5|EXPORT|NULL"), "weak_cipher",
match(version, "TLSv10|TLSv11|SSLv3"), "weak_version",
match(cipher, "RSA_WITH"), "non_PFS",
1=1, "ok")
| where issue != "ok"
| stats count, values(issue) as issues, latest(_time) as last
by id.resp_h, server_name, cipher, version
| eval last_seen = strftime(last, "%Y-%m-%d %H:%M")
| lookup asset_cmdb.csv ip as id.resp_h OUTPUT business_unit, owner
| sort -count
bcrypt.dll, ncrypt.dll, libcrypto.so ถูก load โดย process ที่ไม่ใช่ browser/service ปกติ = อาจเป็น custom encryptorImageHash event · lookup known_crypto_consumers.csvindex=crowdstrike event_simpleName=ImageHash
(ImageFileName="*bcrypt.dll" OR ImageFileName="*ncrypt.dll"
OR ImageFileName="*libcrypto*" OR ImageFileName="*libssl*")
| stats count, values(ImageFileName) as libs, values(UserName) as users
by ComputerName as host, ParentBaseFileName as parent
| lookup known_crypto_consumers.csv process as parent OUTPUT is_known
| where isnull(is_known)
| where parent NOT IN ("chrome.exe","firefox.exe","svchost.exe","msedge.exe",
"outlook.exe","teams.exe","slack.exe")
| sort -count
known_crypto_consumers.csv จาก baseline 14 วัน · whitelist business apps (SAP, banking client, internal tools)index=zscaler (action=bypass OR ssl_decrypt_action=no_decrypt)
AND (reason IN ("cert_pinning","protocol_quic","unknown_protocol"))
| stats sum(bytes_out) as bytes, dc(url) as url_count
by user, dest_host, reason
| eval mb = round(bytes/1048576, 2)
| where mb > 500
| iplocation dest_host as host
| eval risk = case(
reason="protocol_quic", "medium",
match(dest_host,"(?i)(t\.me|telegram|tor|onion|protonmail|signal)"), "critical",
1=1, "high")
| where risk!="medium"
| sort -mb
Weekly/monthly report · ใช้ใน dashboard KPI ไม่ส่ง alert · sslyze HEC pipeline
| multisearch
[search index=cert_inventory sourcetype=sslyze:scan
earliest=-1d@d latest=@d
| spath server_scan_results{}.scan_commands_results.ssl_2_0_cipher_suites
| stats values(cipher_suite) as today_cipher,
latest(pubkey_size) as today_pksize by hostname]
[search index=cert_inventory sourcetype=sslyze:scan
earliest=-2d@d latest=-1d@d
| stats values(cipher_suite) as yesterday_cipher,
latest(pubkey_size) as yesterday_pksize by hostname]
| eval drift = case(
today_cipher!=yesterday_cipher, "cipher_changed",
today_pksize!=yesterday_pksize, "keysize_changed",
1=1, "same")
| where drift!="same"
| lookup asset_cmdb.csv hostname OUTPUT business_unit, owner
| table hostname, business_unit, drift, today_cipher, yesterday_cipher
index=cert_inventory sourcetype=sslyze:scan earliest=-1d
| eval score = 100
| eval score = if(match(tls_versions,"TLS_1_0|TLS_1_1|SSL"), score-30, score)
| eval score = if(match(cipher_list,"RSA_WITH|EXPORT|RC4|MD5|3DES"), score-25, score)
| eval score = if(pubkey_algorithm="rsaEncryption" AND pubkey_size<3072,
score-20, score)
| eval score = if(pubkey_algorithm="ecPublicKey" AND match(curve,"prime256"),
score-15, score)
| eval score = if(supports_pqc_hybrid="true", score+10, score)
| eval score = max(0, min(100, score))
| lookup asset_cmdb.csv hostname OUTPUT business_unit
| stats avg(score) as overall_score, count by hostname, business_unit
| sort score
index=netskope sourcetype="netskope:application:json"
| stats count, values(cipher_suite) as ciphers,
latest(_time) as last_seen
by app, app_risk, app_category
| where match(mvjoin(ciphers,","), "(?i)RSA_WITH|TLS_RSA|EXPORT|RC4|3DES")
| eval last_seen = strftime(last_seen, "%Y-%m-%d")
| table app, app_risk, app_category, ciphers, count, last_seen
| sort -count
| multisearch
[search index=crowdstrike CommandLine="*7z*-p*"
OR CommandLine="*openssl enc*" earliest=-7d
| eval signal="edr_staging", weight=3
| stats max(_time) as last_seen, values(signal) as signals,
values(weight) as weights by ComputerName as host]
[search index=zscaler urlcategory IN ("personal_storage","shadow_it")
earliest=-7d
| eval gb_up=bytes_out/1073741824
| stats sum(gb_up) as gb by user
| where gb>2
| eval signal="casb_upload", weight=2
| rename user as host
| stats max(_time) as last_seen, values(signal) as signals,
values(weight) as weights by host]
[search index=cert_inventory sourcetype=sslyze:scan
earliest=-7d
cipher_list IN ("*RSA_WITH*","*RC4*","*DES*")
| eval signal="weak_crypto", weight=1
| rename hostname as host
| stats max(_time) as last_seen, values(signal) as signals,
values(weight) as weights by host]
| stats sum(weight) as risk_score, values(signals) as signals,
max(last_seen) as last_seen by host
| where risk_score >= 4
| eval last_seen = strftime(last_seen, "%Y-%m-%d %H:%M")
| lookup asset_cmdb.csv hostname as host OUTPUT business_unit, criticality
| sort -risk_score
Drop-in templates · save ใน $SPLUNK_HOME/etc/apps/ecop_pqc/local/
# Shannon entropy macro for DNS exfil detection [shannon_entropy(1)] args = field definition = eval _chars=split($field$,"") \ | stats count by _chars \ | eventstats sum(count) as total \ | eval p=count/total, ent=-p*log(p,2) \ | stats sum(ent) as entropy # PQC vulnerable algorithm regex [pqc_weak_cipher] definition = (RSA_WITH|EXPORT|RC4|MD5|3DES|DES_) # PQC vulnerable TLS versions [pqc_weak_version] definition = (TLSv10|TLSv11|SSLv2|SSLv3) # Foreign ASN whitelist (cloud + business partner) [trusted_foreign_asn] definition = asn IN (16509,14618,8075,15169,13335,32934) # AWS, Azure, Google, Cloudflare, Meta
ip,hostname,business_unit,criticality,owner,environment 10.1.1.100,bank-app-prod-01,banking_core,critical,team-banking@ecop.co.th,production 10.1.1.101,bank-app-prod-02,banking_core,critical,team-banking@ecop.co.th,production 10.2.1.50,hr-portal-01,hr,medium,team-hr@ecop.co.th,production 10.3.1.10,build-server-01,it_ops,low,team-devops@ecop.co.th,development
asn,partner,relationship 16509,aws,cloud_provider 8075,microsoft_azure,cloud_provider 15169,google_cloud,cloud_provider 13335,cloudflare,cdn 14618,aws_alt,cloud_provider 32934,meta,saas_partner
process,is_known,notes chrome.exe,yes,web browser firefox.exe,yes,web browser msedge.exe,yes,web browser outlook.exe,yes,email client teams.exe,yes,communications slack.exe,yes,communications svchost.exe,yes,windows service host java.exe,yes,java runtime (review case by case) python.exe,yes,python runtime (review case by case) node.exe,yes,nodejs runtime
process,is_authorized,notes ssh.exe,yes,ssh client sshd,yes,ssh daemon ssh-agent,yes,key agent terraform,yes,iac tool ansible-playbook,yes,config mgmt git.exe,yes,version control with ssh nginx,yes,web server reads cert apache2,yes,web server reads cert haproxy,yes,load balancer openssl,yes,crypto tooling
[D1 - HNDL Egress Volume Anomaly] search = # paste D1 SPL here cron_schedule = 5 * * * * dispatch.earliest_time = -1h dispatch.latest_time = now is_scheduled = 1 alert.severity = 5 action.email = 1 action.email.to = soc@ecop.co.th action.email.subject = [PQC-P0] HNDL Egress Anomaly $result.host$ [D5 - Crypto Staging Upload] search = # paste D5 SPL here cron_schedule = */5 * * * * is_scheduled = 1 alert.severity = 5 action.script = 1 action.script.filename = create_incident.py [D13 - Daily Crypto Drift] search = # paste D13 SPL here cron_schedule = 0 6 * * * dispatch.earliest_time = -1d@d dispatch.latest_time = @d is_scheduled = 1 action.email = 1 action.email.to = crypto-team@ecop.co.th [D14 - PQC Score Daily] search = # paste D14 SPL here cron_schedule = 0 8 * * * is_scheduled = 1 # no email - feeds dashboard only [D15 - Multi-Stage Kill Chain] search = # paste D15 SPL here cron_schedule = 30 9 * * * is_scheduled = 1 alert.severity = 4 action.email = 1 action.email.to = soc-mgmt@ecop.co.th, ciso@ecop.co.th action.email.subject = [PQC-Weekly] Multi-Stage HNDL Kill Chain
XML Studio outline · panel layout · drag-drop import ได้
| Panel | Source | Visualization | Position |
|---|---|---|---|
| HNDL Kill Chain alerts | D15 | Single value + sparkline | Top-left |
| Top 10 risky hosts (volume) | D1 | Bar chart | Top-right |
| Top 10 risky users (CASB upload) | D9 | Bar chart | Mid-left |
| Long-lived foreign TLS sessions | D2 | Table (live, top 20) | Mid-right |
| Crypto staging events (real-time) | D5 | Table (last 24h) | Bottom-left |
| KMS access spike | D12 | Line chart (24h) | Bottom-right |
| Panel | Source | Visualization | Audience |
|---|---|---|---|
| PQC Readiness Score trend (30d) | D14 | Line chart | CISO / board |
| PQC Score heatmap by business_unit | D14 | Heat map | Engineering managers |
| Daily Crypto Drift count | D13 | Single value + delta | Crypto team |
| Weak Cipher endpoints (current) | D4 | Table with owner column | System owners |
| SaaS vendor PFS gap | D11 | Bar chart with risk score | Procurement / vendor mgmt |
| Vulnerable algorithm trend (90d) | D4 stats | Stacked area | Crypto team |
<form theme="dark" version="1.1">
<label>PQC Risk Operations</label>
<description>Real-time HNDL detection · refresh every 5m</description>
<fieldset>
<input type="time" token="tr">
<label>Time range</label>
<default><earliest>-24h</earliest><latest>now</latest></default>
</input>
</fieldset>
<row>
<panel>
<title>Multi-stage HNDL alerts (D15)</title>
<single>
<search>
<query>| inputlookup d15_results.csv | where risk_score>=4 | stats count</query>
<earliest>$tr.earliest$</earliest>
<latest>$tr.latest$</latest>
</search>
<option name="colorBy">value</option>
<option name="rangeColors">["0x65a637","0xf7bc38","0xd93f3c"]</option>
<option name="rangeValues">[1,5]</option>
</single>
</panel>
<!-- repeat for D1, D9 etc -->
</row>
</form>
0.5-1 FTE detection engineer · ~80-100 ชม. · ผลลัพธ์: 15 detection + 2 dashboard live
bro:conn / bro:ssl ingestion ใน Splunkasset_cmdb.csv + business_partner_asn.csv