Risk Gifts, An Early Present

Earlier this year we gave a talk on an effort we call ShadowHawk. It is, at the core, an abstracted layer between detections and alerts and is using a risk score and risk objects to communicate between the two. We go into several of the advantages as well as the struggles. If interested, check out the talk:

Recording - https://conf.splunk.com/files/2018/recordings/say-goodbye-to-your-sec1479.mp4

Slides - https://static.rainfocus.com/splunk/splunkconf18/sess/1523456018499001lxCD/finalPDF/SEC1479_SayGoodbyeToYourBig_Final_1538509127390001SxPF.pdf

But that is not what this post is going to cover. What we are going to cover is a working risk rule you can deploy and the search macros behind it!


This risk rule leverages the Web datamodel in Splunk but you can modify it to fit your approach. The goal of this detection is to identify web traffic not using port 80 or 443 then assign risk to the source computer and destination domain/ip.

| tstats `summariesonly` count from datamodel=Web where Web.dest_port!="80" Web.dest_port!="443" by Web.url, Web.src, Web.dest_port
| `drop_dm_object_name("Web")`

| eval src_ip=src
| lookup dnslookup clientip as src_ip OUTPUT clienthost as src
| eval src=replace(src,".yourdomain.com","")
| fillnull value="" src
| eval src=if(src=="",src_ip,src)

| eval list="mozilla" 
| `ut_parse_extended(url,list)`

| rename src as src_system, ut_domain as dest_system
| fields url dest_system src_system src_ip dest_port count

| eval rule_impact="Low"
| eval rule_confidence="Low"
| eval rule_phases="command_and_control"
| eval rule_name="Non-Standard Ports - Web Traffic"

| `risk_modifier_system(src_system)`
| `risk_score(rule_impact,rule_confidence,risk_modifier_count)`

| eval risk_object_type="system"
| `risk_attribution(src_system,risk_object_type,risk_score,rule_phases,rule_name)`

| eval risk_object_type="external_system"
| `risk_attribution(dest_system,risk_object_type,risk_score,rule_phases,rule_name)`

Search Macros

In the search above you will see some custom risk_* macros and we wanted to share those as well.

Risk Modifier Determination - User:
MacroName: risk_modifier_user(1)
join type=left $user$ [| inputlookup ldapsearch_identities.csv 
| eval risk_user=upper(identity)
| fields risk_user category priority bunit watchlist
| eval object_type="user"

| eval mod_count=0
| eval mod_count=if(like(category,"%privileged%"),mod_count+1,mod_count)
| eval mod_count=if(like(category,"%service-account%"),mod_count+1,mod_count)
| eval mod_count=if(like(bunit,"%Executives%"),mod_count+1,mod_count)
| eval mod_count=if(watchlist="true",mod_count+1,mod_count)

| stats sum(mod_count) as risk_modifier_count BY risk_user
| rename risk_user AS $user$

| fillnull value=0 risk_modifier_count

Risk Modifier Determination - System:
MacroName: risk_modifier_system(1)
join type=left $system$ [| inputlookup ldapsearch_assets.csv 
| eval risk_system=upper(nt_host)
| fields risk_system category priority lastLogonTimestamp
| eval object_type="system"

| eval lastLogonTimestamp_epoch=strptime(lastLogonTimestamp,"%Y-%m-%d %H:%M %p")
| eval logon_cutoff=relative_time(now(), "-1y@d")
| where lastLogonTimestamp_epoch>logon_cutoff

| join type=left risk_system[search index=VULNSCAN vendor_severity=Critical earliest=-30d@d
| stats count by dns
| eval temp_system=split(dns,".")
| eval risk_system=upper(mvindex(temp_system,0))
| rename count as count_vuln_critical
| fields risk_system count_vuln_critical
| fillnull value=0 count_vuln_critical

| eval mod_count=0
| eval mod_count=if(like(category,"%privileged%"),mod_count+1,mod_count)
| eval mod_count=if(like(category,"%service-account%"),mod_count+1,mod_count)
| eval mod_count=if(object_type="system" AND (priority="critical" OR priority="high"),mod_count+1,mod_count)
| eval mod_count=mod_count+count_vuln_critical

| stats sum(mod_count) as risk_modifier_count BY risk_system
| rename risk_system AS $system$

| fillnull value=0 risk_modifier_count

Risk Modifier Determination - Combined:
MacroName: risk_modifier(2)
| rename risk_modifier_count AS risk_modifier_count_system

| `risk_modifier_user($user$)`
| rename risk_modifier_count AS risk_modifier_count_user

| eval risk_modifier_count=risk_modifier_count_user + risk_modifier_count_system

Risk Score Calculation:
MacroName: risk_score(3)
eval confidence=lower($confidence$)
| eval risk_confidence_score=case(confidence=="low", 30,confidence=="medium",60,confidence=="high",100)

| eval impact=lower($impact$)
| eval risk_impact_score=case(impact=="info", 20,impact=="low",40,impact=="medium",60,impact=="high",80,impact=="critical",100)

| eval risk_score=round(((risk_confidence_score/100)*risk_impact_score)*(($modifiers$*.25)+1))
| fields - impact confidence risk_impact_score risk_confidence_score

Risk Attribution:
MacroName: risk_attribution(5)
eval risk_object=$object$, risk_object_type=$object_type$, risk_score=$risk_score$, attack_phase=$attack_phase$, search_name=$risk_rule_name$

| collect index=risk


We realize this is post is a bit rough but really wanted to help start the conversation with others trying to find a solution to alert fatigue and driving at much higher fidelity alerts. With this in place we saw twice the detections with a 70% reduction in alerts.