├── README.md ├── active-response ├── add_to_cdb.sh ├── alert2chat.sh ├── bhr.sh ├── cif.sh ├── command_search.sh ├── cymru_lookup.sh ├── deb_lookup.sh ├── ldap_lookup.sh ├── makelists.sh ├── puppetdb_lookup.sh ├── rpm_lookup.sh ├── rule-all.sh ├── syscheck-all.sh ├── syscheck2chat.sh ├── time_lookup.sh ├── virus_total.py └── virustotal_lookup.sh ├── decoders ├── active_response.xml ├── kerberos_decoder.xml └── sudo.xml ├── munin ├── ossec_ar_stats ├── ossec_munin.png ├── ossec_stats ├── ossec_top_groups └── ossec_top_rules ├── nagios ├── check_ossec.py └── check_ossec.sh └── rules ├── kerberos_rules.xml ├── organization_ar_rules.xml ├── organization_rules.xml ├── organization_syscheck_rules.xml └── overwrite_rules.xml /README.md: -------------------------------------------------------------------------------- 1 | # ossec-tools 2 | Scripts and integrations for OSSEC. With the following code and configurations 3 | we transformed our OSSEC deployment from an overwhelming mess of alerts to a 4 | manageable and useful system where most of the alerts we actually care about. 5 | 6 | It includes things like code to block hosts at our perimeter, create intelligence feeds from 7 | the logs, and only e-mail alerts when a file has changed that is not managed by Puppet and is different from 8 | what's reported by the package manager's checksums - meaning no alerts for files changed after package updates :) 9 | 10 | ## Active Response 11 | Custom AR scripts 12 | 13 | * active-response/virustotal_lookup.sh/virus_total.py - Look up hash from syscheck alerts in VT database 14 | * active-response/cymru_lookup.sh - Look up hash from sysheck alerts in Team Cymru Malware Hash Registery 15 | * active-response/puppetdb_lookup.sh - Look up managed files in PuppetDB 16 | * active-response/rpm_lookup.sh - Look up files that changed from RPM install (must be present on agents) 17 | * active-response/deb_lookup.sh - Lookup file that changes from DEB install (must be present on agents) 18 | * active-response/time_lookup.sh - Check if system clock is off or time zone differs for analyzed logs 19 | * active-response/ldap_lookup.sh - Lookup employee usernames in LDAP database 20 | * active-response/command_search.sh - Search for malicious commands across logs 21 | * active-response/cif.sh - Create intelligence feed from alerts 22 | * active-response/bhr.sh - Block hosts at perimeter using Black Hole Router by Justin Azoff 23 | * active-response/add_to_cdb.sh - Add entries from alerts to CDB database (only collect system users at this time) 24 | * active-response/rule-all.sh - Run many of the above scripts 25 | * active-response/syscheck-all.sh - Run many of the syscheck scripts 26 | 27 | Many of the scripts generate logs which are fed into OSSEC, decoded, and analyzed. 28 | The decoders and rules are in the respective sections below. Don't include anything you don't need. 29 | For example, if you don't use puppet for configuration management or run Debian system don't include 30 | the puppetdb_lookup or rpm_lookup configurations. 31 | 32 | 1. Server: copy all the files above to `$OSSEC/active-response/bin/` directory. 33 | 2. Server: edit `$OSSEC/etc/ossec.conf` with the AR and localfile configuration. 34 | ``` 35 | 36 | syscheck_all 37 | syscheck-all.sh 38 | filename 39 | 40 | 41 | 42 | ip_all 43 | rule-all.sh 44 | srcip 45 | 46 | 47 | 48 | rule_all 49 | rule-all.sh 50 | 51 | 52 | 53 | 54 | rpm_lookup 55 | rpm_lookup.sh 56 | filename 57 | 58 | 59 | 60 | deb_lookup 61 | deb_lookup.sh 62 | filename 63 | 64 | 65 | 66 | restart-ossec 67 | restart-ossec.sh 68 | 69 | 70 | 71 | 72 | syscheck_all 73 | syscheck,, 74 | 5 75 | server 76 | 77 | 78 | 79 | rpm_lookup 80 | syscheck,, 81 | 5 82 | local 83 | 84 | 85 | 86 | deb_lookup 87 | syscheck,, 88 | 5 89 | local 90 | 91 | 92 | 93 | ip_all 94 | 4 95 | server 96 | 97 | 98 | 99 | rule_all 100 | 4 101 | server 102 | 103 | 104 | 105 | restart-ossec 106 | local 107 | 110003 108 | 109 | 110 | 111 | syslog 112 | /var/ossec/logs/puppetdb_lookup.log 113 | 114 | 115 | 116 | syslog 117 | /var/ossec/logs/virustotal_lookup.log 118 | 119 | 120 | 121 | syslog 122 | /var/ossec/logs/cymru_lookup.log 123 | 124 | 125 | 126 | syslog 127 | /var/ossec/logs/rpm_lookup.log 128 | 129 | 130 | 131 | syslog 132 | /var/ossec/logs/deb_lookup.log 133 | 134 | 135 | 136 | syslog 137 | /var/ossec/logs/command_search.log 138 | 139 | 140 | 141 | syslog 142 | /var/ossec/logs/time_lookup.log 143 | 144 | ``` 145 | 3. Agent: copy `rpm_lookup.sh` and `deb_lookup.sh` to `$OSSEC/active-response/bin/` and localfile config to `$OSSEC/etc/ossec.conf`. 146 | ``` 147 | 148 | syslog 149 | /var/ossec/logs/rpm_lookup.log 150 | 151 | 152 | 153 | syslog 154 | /var/ossec/logs/deb_lookup.log 155 | 156 | ``` 157 | 158 | 4. Server: Add `organization_ar_rules.xml` to `$OSSEC/rules/` and include the rules in `$OSSEC/etc/ossec.conf`. 159 | 5. Server: Add `active_response.xml` decoder to `$OSSEC/etc/` and include the decoder in `$OSSEC/etc/ossec.conf`. 160 | 6. Agent/Server: Before restarting create the log files so OSSEC begins reading them e.g. `touch $OSSEC/logs/{deb,rpm}_lookup.sh` 161 | 7. Restart the server and agent OSSEC software 162 | 163 | ## Decoders 164 | Custom decoders 165 | 166 | * decoders/kerberos.xml - Decoder for ksu and kadmind logs 167 | * decoders/active_response.xml - Decoder for logs generated from our active response scripts 168 | * decoders/sudo.xml - Improved sudo decoder 169 | 170 | ## Rules 171 | Custom rules 172 | 173 | * rules/kerberos_rules.xml - Kerberos rules 174 | * rules/organization_ar_rules.xml - Active response rules for our AR scripts 175 | * rules/organization_syscheck_rules.xml - Examples of syscheck rules 176 | * rules/organization_rules.xml - Examples of standard log rules 177 | * rules/overwrite_rules.xml - Existing rules that contain modifications 178 | 179 | ## Munin 180 | Plugins to graph OSSEC stats for Munin 181 | 182 | * munin/ossec_ar_stats - Graph active response script usage 183 | * munin/ossec_stats - Graph alert counts by 3 major types of alerts: rules, syscheck, or rootcheck 184 | * munin/ossec_top_groups - Graph alert counts by top 10 rule categories 185 | * munin/ossec_top_rules - Graph alert counts by top 10 rule descriptions 186 | 187 | Location 188 | ``` 189 | $ ls /etc/munin/plugins/ossec_* 190 | /etc/munin/plugins/ossec_ar_stats /etc/munin/plugins/ossec_stats /etc/munin/plugins/ossec_top_groups /etc/munin/plugins/ossec_top_rules 191 | ``` 192 | 193 | Configuration 194 | ``` 195 | $ cat /etc/munin/plugin-conf.d/ossec_* 196 | [ossec_ar_stats] 197 | user root 198 | [ossec_stats] 199 | user root 200 | [ossec_top_groups] 201 | user root 202 | [ossec_top_rules] 203 | user root 204 | ``` 205 | 206 | ![Munin Graphs](https://raw.githubusercontent.com/ncsa/ossec-tools/master/munin/ossec_munin.png) 207 | 208 | ## Nagios 209 | Plugins to check OSSEC services. There's two scripts but the python script `check_ossec.py` is newer and has more features. 210 | Examples of both are listed below. 211 | 212 | * Check status of OSSEC services excluding active response service (execd) 213 | * `./check_ossec.py -T status` 214 | * Check that all agents are connected except host1 215 | * `./check_ossec.py -T connected --skip host1.blah.org` 216 | * Check that syscheck has been completed for all agents in the last 12 hours 217 | * `./check_ossec.py -T syscheck -c 12 -w 6` 218 | * Check that rootkit checks have been completed for all agents in the last 3 hours 219 | * `./check_ossec.py -T rootcheck -c 12 -w 6` 220 | * Check status of OSSEC services excluding active response i.e. execd 221 | * `./check_ossec.sh -s execd` 222 | * Check status of OSSEC agent 223 | * `./check_ossec.sh -a server1` 224 | * Check status of multiple OSSEC agents 225 | * `./check_ossec.sh -a "server1,server2,station3"` 226 | * Report critical if more than 3 agents are offline and warning if at least 1 is offline. 227 | * `./check_ossec.sh -c 3 -w 1` 228 | -------------------------------------------------------------------------------- /active-response/add_to_cdb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Jon Schipp 3 | PRE="$(date +"%Y-%m-%dT%T.%6N%:z") $(hostname -s) $(basename $0):" 4 | LOG="../logs/add_to_cdb.log" 5 | AR_LOG="${PWD}/../logs/active-responses.log" 6 | CDB="/var/ossec/lists/system_users.list" 7 | ACTION=$1 8 | USER=$2 9 | IP=$3 10 | ALERTID=$4 11 | RULEID=$5 12 | AGENT=$6 13 | SERVICE=$7 14 | FILENAME=$8 15 | 16 | die(){ 17 | [[ -d $(dirname $LOG) ]] && printf "$PRE $*\n" | tee -a $LOG 18 | exit 1 19 | } 20 | 21 | log(){ 22 | [[ -d $(dirname $LOG) ]] && printf "$PRE $*\n" | tee -a $LOG 23 | exit 0 24 | } 25 | 26 | check_args(){ 27 | local argc 28 | argc="$1" 29 | [[ $argc -ge 5 ]] || die "ERROR: Not enough arguments given" 30 | } 31 | 32 | get_alert(){ 33 | local alert 34 | alert=$(awk -v ts=$ALERTID 'BEGIN { RS=""; ORS="\n" } $0 ~ ts { print }' ${PWD}/../logs/alerts/alerts.log) 35 | [[ "$alert" ]] || return 1 36 | printf "$alert\n" 37 | } 38 | 39 | is_system_uid(){ 40 | local uid="$1" 41 | [[ "$uid" ]] || die "ERROR: ${uid:-uid} was not found" 42 | [[ $uid -le 500 && $uid -gt 0 ]] || return 1 43 | return 0 44 | } 45 | 46 | get_uid_from_alert(){ 47 | local alert="$@" 48 | local uid_field 49 | local uid 50 | uid_field=$(printf "$alert\n" | grep -o 'UID=[0-9]\+') 51 | uid=${uid_field#*=} 52 | printf "$uid\n" 53 | } 54 | 55 | get_username_from_alert(){ 56 | local alert="$@" 57 | local name_field 58 | local username 59 | name_field=$(printf "$alert\n" | grep -o 'name=[a-zA-Z0-9_]\+') 60 | name=${name_field#*=} 61 | printf "$name\n" 62 | } 63 | 64 | send_cdb(){ 65 | local username="$1" 66 | if [[ -w "$CDB" ]]; then 67 | printf "${username}:system\n" >> $CDB 68 | fi 69 | } 70 | 71 | check_cdb(){ 72 | local username="$1" 73 | if [[ -r "$CDB" ]]; then 74 | grep -q "${username}:system" $CDB || return 1 75 | fi 76 | exit 0 77 | } 78 | 79 | # Check for arguments 80 | check_args $# 81 | 82 | LOCAL=$(dirname $0); 83 | cd $LOCAL 84 | cd ../ 85 | PWD=$(pwd) 86 | 87 | # Logging the call 88 | [[ -f "$AR_LOG" ]] && printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> $AR_LOG 89 | 90 | # Only work on rule 5902 'new user' 91 | [[ $RULEID -eq 5902 ]] || die "ERROR: ${RULEID:-ruleid} did not match" 92 | 93 | # Getting full alert 94 | ALERT=$(get_alert) || die "ERROR: get_alert: No alert found matching timestamp $ALERTID" 95 | 96 | # Extract info from alert 97 | USERID="$(get_uid_from_alert "$ALERT")" 98 | is_system_uid "$USERID" || die "ERROR: UID $USERID is not in the system range" 99 | USER="$(get_username_from_alert "$ALERT")" 100 | 101 | # Check if we've looked up this user previously 102 | check_cdb "$USER" || send_cdb "$USER" 103 | 104 | # Enable 105 | /var/ossec/bin/ossec-makelists -c /var/ossec/etc/ossec.conf 106 | 107 | # Log 108 | log "OK: Added system user $USER to $CDB" 109 | -------------------------------------------------------------------------------- /active-response/alert2chat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Author: Jon Schipp 3 | CHAT=/usr/local/bin/ircsay 4 | CHANNEL="#ossec-alerts" 5 | ACTION=$1 6 | USER=$2 7 | IP=$3 8 | ALERTID=$4 9 | RULEID=$5 10 | 11 | # Check for chat program 12 | [[ -f $CHAT ]] || exit 1 13 | 14 | # Skip rules to avoid flooding of alerts of less importance 15 | # Syscheck 16 | [[ "$*" =~ syscheck ]] && exit 17 | # Login & SSH attempts 18 | [[ $RULEID =~ ^(570[1-3]|5710)$ ]] && exit 19 | # System 20 | [[ $RULEID =~ ^(2933|5113)$ ]] && exit 21 | 22 | LOCAL=$(dirname $0); 23 | cd $LOCAL 24 | cd ../ 25 | PWD=$(pwd) 26 | 27 | # Logging the call 28 | printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> ${PWD}/../logs/active-responses.log 29 | 30 | # Getting full alert 31 | awk -v ts=$ALERTID 'BEGIN { RS=""; ORS="\n" } $0 ~ ts { print }' ${PWD}/../logs/alerts/alerts.log | $CHAT "$CHANNEL" - 32 | -------------------------------------------------------------------------------- /active-response/bhr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | TOKEN="" 3 | URL="https://bhr.org.tld/bhr/api/block" 4 | CHAT=/usr/local/bin/ircsay 5 | CHANNEL="#host-blocks" 6 | IP=$3 7 | RULEID=$5 8 | MESSAGE='OSSEC Rule '$RULEID'' 9 | 10 | TIMEOUT=900 11 | 12 | # Requires IP 13 | [[ $IP ]] || exit 14 | 15 | # Don't block your nets 16 | [[ $IP =~ ^10\.1\.[0-9]{1,3}\.[0-9]{1,3}$ ]] && exit 17 | 18 | # Skip RFC 1918 addresses 19 | [[ $IP =~ ^10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] && exit 20 | [[ $IP =~ ^172\.16\.[0-9]{1,3}\.[0-9]{1,3}$ ]] && exit 21 | [[ $IP =~ ^192\.168\.[0-9]{1,3}\.[0-9]{1,3}$ ]] && exit 22 | 23 | # Only block for these rules 24 | [[ $RULEID =~ ^(5703|5712|31153|100022)$ ]] || exit 25 | 26 | # Logging the call 27 | printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> ${PWD}/../logs/active-responses.log 28 | 29 | curl -X POST -H "Authorization: Token ${TOKEN}" $URL \ 30 | --data-urlencode source="ossec" \ 31 | --data-urlencode duration="${TIMEOUT}" \ 32 | --data-urlencode why="${MESSAGE}" \ 33 | --data-urlencode cidr="${IP}/32" \ 34 | --data-urlencode autoscale="1" 35 | 36 | [[ -f $CHAT ]] && printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" | $CHAT "$CHANNEL" - 37 | -------------------------------------------------------------------------------- /active-response/cif.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Generates intel feed entries for CIF 3 | # Author: Jon Schipp 4 | CHAT=/usr/local/bin/ircsay 5 | CHANNEL="#feeds" 6 | ACTION=$1 7 | USER=$2 8 | IP=$3 9 | ALERTID=$4 10 | RULEID=$5 11 | DESCRIPTION=$(grep -h -A 10 ']' '/description/ { printf("%s", $3) } END { printf("\n") }') 12 | FILE=/var/www/html/ossec.csv 13 | 14 | # Logging the call 15 | echo "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8" >> ${PWD}/../logs/active-responses.log 16 | 17 | [[ $IP ]] || exit 18 | 19 | # RFC 1918 20 | [[ $IP =~ ^10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] && exit 21 | [[ $IP =~ ^172\.16\.[0-9]{1,3}\.[0-9]{1,3}$ ]] && exit 22 | [[ $IP =~ ^192\.168\.[0-9]{1,3}\.[0-9]{1,3}$ ]] && exit 23 | # Add other networks to skip 24 | 25 | printf "${IP},OSSEC,${RULEID},${DESCRIPTION}\n" >> $FILE 26 | [[ -f $CHAT ]] && printf "${IP},OSSEC,${RULEID},${DESCRIPTION}\n" | $CHAT "$CHANNEL" - 27 | -------------------------------------------------------------------------------- /active-response/command_search.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Jon Schipp 3 | PRE="$(date +"%Y-%m-%dT%T.%6N%:z") $(hostname -s) $(basename $0):" 4 | LOG="../logs/command_search.log" 5 | AR_LOG="${PWD}/../logs/active-responses.log" 6 | ACTION=$1 7 | USER=$2 8 | IP=$3 9 | ALERTID=$4 10 | RULEID=$5 11 | AGENT=$6 12 | SERVICE=$7 13 | FILENAME=$8 14 | 15 | die(){ 16 | echo "$PRE $*" | tee -a -- "$LOG" 17 | exit 1 18 | } 19 | 20 | log(){ 21 | echo "$PRE $*" >> "$LOG" 22 | exit 0 23 | } 24 | 25 | check_args(){ 26 | local argc 27 | argc="$1" 28 | [[ $argc -ge 5 ]] || die "ERROR: Not enough arguments given" 29 | } 30 | 31 | get_alert(){ 32 | local alert 33 | local alertid="$1" 34 | alert=$(awk -v ts=${alertid}: 'BEGIN { RS=""; ORS="\n" } $0 ~ ts { print }' ${PWD}/../logs/alerts/alerts.log) 35 | [[ "$alert" ]] || return 1 36 | echo "$alert" 37 | } 38 | 39 | get_msg(){ 40 | local alert="$@" 41 | msg=$(echo "$alert" | grep '^[A-Z][a-z]\+ [0-9]\+ ') 42 | echo "$msg" 43 | } 44 | 45 | is_skip(){ 46 | local msg="$@" 47 | # If we find pattern return 1 to skip logging 48 | echo "$msg" | egrep -q '\]: Updated: Bad protocol version identification|Bad protocol version identification| [iI]nvalid user |mess id 0x' && return 1 49 | return 0 50 | } 51 | 52 | search_commands(){ 53 | local alert="$@" 54 | local p='[^a-zA-Z0-9\.=]' 55 | local na='[^a-zA-Z]' # No alpha 56 | local nan='[^a-zA-Z0-9]' # No alpha & numeric 57 | local recon="${p}id${p}|uname -a |uname${na}|${p}last${na}|${p}w${nan}|whoami|${p}who${na}|wtmp|btmp|shadow" 58 | local shells="${p}sh -i|bash -i |${p}bash |${p}sh -c |${p}sh " 59 | local escalation="${p}wget|${p}ftp |${p}curl|${p}fetch |${p}john${na}|${p}hashcat|crack|sniff|tcpdump|shark " 60 | local exploitation="msfpayload|msfcli|metasploit|meterpreter" 61 | local hiding="shred|HIST|history -" 62 | local commands="${recon}|${shells}|${escalation}|${hiding}" 63 | msg=$(get_msg "$alert") 64 | echo "$msg" | egrep -q -- "$commands" || return 1 65 | is_skip "$msg" || return 1 # Skip logs that we cannot match easily with patterns 66 | match="$(echo "$msg" | egrep -o -- "$commands")" 67 | echo "Matched $match in $msg" 68 | } 69 | 70 | # Check for arguments 71 | check_args $# 72 | 73 | LOCAL=$(dirname $0); 74 | cd $LOCAL 75 | cd ../ 76 | PWD=$(pwd) 77 | 78 | # Logging the call 79 | [[ -f "$AR_LOG" ]] && echo "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8" >> $AR_LOG 80 | 81 | # Getting full alert 82 | ALERT=$(get_alert "$ALERTID") || exit 83 | # Skip alerts with multiple log lines 84 | [[ "$ALERT" =~ [Mm]ultiple ]] && exit 85 | # Get log messages if found command 86 | MSG=$(search_commands "$ALERT") || exit 87 | 88 | # Log 89 | log "WARNING: $MSG" 90 | -------------------------------------------------------------------------------- /active-response/cymru_lookup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Jon Schipp 3 | PRE="$(date +"%Y-%m-%dT%T.%6N%:z") $(hostname -s) $(basename $0):" 4 | LOG="../logs/cymru_lookup.log" 5 | AR_LOG="${PWD}/../logs/active-responses.log" 6 | CDB="/var/ossec/lists/hashes.list" 7 | ACTION=$1 8 | USER=$2 9 | IP=$3 10 | ALERTID=$4 11 | RULEID=$5 12 | AGENT=$6 13 | SERVICE=$7 14 | FILENAME=$8 15 | 16 | die(){ 17 | printf "$PRE $*\n" | tee -a $LOG 18 | exit 1 19 | } 20 | 21 | log(){ 22 | printf "$PRE $*\n" | tee -a $LOG 23 | exit 0 24 | } 25 | 26 | send_cdb(){ 27 | local checksum 28 | local file 29 | checksum="$1" 30 | file="$2" 31 | if [[ -w "$CDB" ]]; then 32 | printf "${checksum}:${file:-empty}\n" >> $CDB 33 | fi 34 | } 35 | 36 | check_cdb(){ 37 | local checksum 38 | checksum="$1" 39 | if [[ -r "$CDB" ]]; then 40 | grep -q "$checksum" $CDB && die "Hash $checksum has been looked up previously" 41 | fi 42 | } 43 | 44 | LOCAL=$(dirname $0); 45 | cd $LOCAL 46 | cd ../ 47 | PWD=$(pwd) 48 | 49 | # Logging the call 50 | [[ -f "$AR_LOG" ]] && printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> $AR_LOG 51 | 52 | # Getting full alert 53 | ALERT=$(awk -v ts=${ALERTID}: 'BEGIN { RS=""; ORS="\n" } $0 ~ ts { print }' ${PWD}/../logs/alerts/alerts.log) 54 | 55 | # Obtain hash 56 | SHA1=$(printf "$ALERT\n" | grep -o '[a-zA-Z0-9]\{40\}' | tail -n 1) 57 | [[ $SHA1 ]] || die "ERROR: No hash found (${ALERTID:-empty})" 58 | 59 | # Check if we've looked up this hash previously 60 | check_cdb "$SHA1" 61 | 62 | # Obtain other info for logging 63 | [[ "$FILENAME" ]] || FILENAME=$(printf "$ALERT\n" | awk -F "['']" '/^File|changed for:/ { print $2 }') 64 | [[ "$AGENT" ]] || AGENT=$(printf "$ALERT\n" | awk -F "[()]" '/syscheck/ { print $2 }' | tail -n 1) 65 | 66 | # Lookup 67 | result=$(timeout 1s dig +short ${SHA1}.malware.hash.cymru.com A) 68 | 69 | # Alert or exit 70 | [[ "$result" =~ '127.0.0.' ]] && log "WARNING: Malicious hash found for ${FILENAME:-(empty)} ($SHA1) on ${AGENT:-(empty)}" 71 | send_cdb "$SHA1" "$FILENAME" 72 | die "OK: No match found for ${FILENAME:-(empty)} ($SHA1) on ${AGENT:-(empty)}" 73 | -------------------------------------------------------------------------------- /active-response/deb_lookup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Jon Schipp 3 | PRE="$(date +"%Y-%m-%dT%T.%6N%:z") $(hostname -s) $(basename $0):" 4 | LOG="../logs/deb_lookup.log" 5 | AR_LOG="${PWD}/../logs/active-responses.log" 6 | ALERTS_LOG="${PWD}/../logs/alerts/alerts.log" 7 | ACTION=$1 8 | USER=$2 9 | IP=$3 10 | ALERTID=$4 11 | RULEID=$5 12 | AGENT=$6 13 | [[ "$7" =~ syscheck ]] && FILENAME=$8 || FILENAME=$7 14 | 15 | die(){ 16 | printf "$PRE $*\n" | tee -a $LOG 17 | exit 1 18 | } 19 | 20 | log(){ 21 | printf "$PRE $*\n" | tee -a $LOG 22 | exit 0 23 | } 24 | 25 | get_hash(){ 26 | # Obtain hash from lines e.g. "2eb1a3e346933962bdfbb7b118404b68 /bin/ls" 27 | local line="$@" 28 | printf "${line% *}\n" 29 | } 30 | 31 | deb_lookup() { 32 | local file="$1" 33 | results="$(dpkg -S "$file" 2>/dev/null)" 34 | [[ "$results" ]] || die "WARNING: $file not installed by package" 35 | pkg="${results%%:*}" 36 | hashfile="/var/lib/dpkg/info/$pkg.md5sums" 37 | [[ -s "$hashfile" ]] || die "ERROR: db $hashfile not available or empty" 38 | match="$(grep -w "${file#/}" "$hashfile")" 39 | db_hash=$(get_hash "$match") 40 | line="$(md5sum "$file")" 41 | fs_hash="$(get_hash "$line")" 42 | [[ "$db_hash" = "$fs_hash" ]] || return 1 43 | return 0 44 | } 45 | 46 | LOCAL=$(dirname $0); 47 | cd $LOCAL 48 | cd ../ 49 | PWD=$(pwd) 50 | 51 | # Logging the call 52 | printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> $AR_LOG 53 | # System must be Debian based 54 | [[ -f /etc/debian_version ]] || exit 0 55 | 56 | # We need a filename 57 | [[ "$FILENAME" ]] || die "No file given" 58 | 59 | deb_lookup "$FILENAME" || die "WARNING: ${FILENAME:-(empty)} differs from DEB database" 60 | log "OK: ${FILENAME:-(empty)} DEB verification passed" 61 | exit 0 62 | -------------------------------------------------------------------------------- /active-response/ldap_lookup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Jon Schipp 3 | PRE="$(date +"%Y-%m-%dT%T.%6N%:z") $(hostname -s) $(basename $0):" 4 | LOG="../logs/ldap_lookup.log" 5 | AR_LOG="${PWD}/../logs/active-responses.log" 6 | ALERTS_LOG="${PWD}/../logs/alerts/alerts.log" 7 | CDB="/var/ossec/lists/system_users.list" 8 | ACTION=$1 9 | USER=$2 10 | IP=$3 11 | ALERTID=$4 12 | RULEID=$5 13 | AGENT=$6 14 | LDAP_SERVER='ldaps://ldap.company.com/' # Replace 15 | OU='dc=blah,dc=company,dc=com' # Replace 16 | 17 | die(){ 18 | printf "$PRE $*\n" | tee -a $LOG 19 | exit 1 20 | } 21 | 22 | log(){ 23 | printf "$PRE $*\n" | tee -a $LOG 24 | exit 0 25 | } 26 | 27 | check_args(){ 28 | local argc 29 | argc="$1" 30 | [[ $argc -ge 5 ]] || die "ERROR: Not enough arguments given" 31 | } 32 | 33 | ldap_lookup(){ 34 | local user="$1" 35 | local server="$2" 36 | local ou="$3" 37 | ldap_info=$(ldapsearch -xLLL -b "$ou" -H "$server" "uid=$user" | awk '/uid: / { print $2 }') 38 | [[ "$ldap_info" ]] || return 1 39 | return 0 40 | } 41 | 42 | get_alert(){ 43 | local alertid="$1" 44 | local alert 45 | alert=$(awk -v ts=${alertid}: 'BEGIN { RS=""; ORS="\n" } $0 ~ ts { print }' ${PWD}/../logs/alerts/alerts.log) 46 | [[ "$alert" ]] || return 1 47 | printf "$alert\n" 48 | } 49 | 50 | get_user(){ 51 | local alert="$@" 52 | local user 53 | [[ "$alert" ]] || return 1 54 | user=$(echo "$alert" | awk '/^User: / { print $2 }') 55 | # Kerberos events and maybe others contain a realm/domain, trim domain 56 | user=${user%%@*} 57 | [[ "$user" ]] || return 1 58 | [[ "$user" == root ]] && return 1 59 | printf "$user\n" 60 | } 61 | 62 | get_ip(){ 63 | local alert="$@" 64 | local ip 65 | [[ "$alert" ]] || return 1 66 | ip=$(echo "$alert" | awk '/^Src IP: / { print $3 }') 67 | [[ "$ip" ]] || return 1 68 | printf "$ip\n" 69 | } 70 | 71 | get_hostname(){ 72 | local alert="$@" 73 | local host 74 | host=$(printf "$alert\n" | awk '/^20[0-9][0-9] [A-Z][a-z][a-z] [0-9][0-9]/ { print $5 }') 75 | # above returns strings as host->ip, we just want the IP 76 | host="${host##*>}" 77 | printf "$host\n" 78 | } 79 | 80 | check_cdb(){ 81 | local username="$1" 82 | if [[ -r "$CDB" ]]; then 83 | grep -q "${username}:" $CDB || return 1 84 | fi 85 | } 86 | 87 | LOCAL=$(dirname $0); 88 | cd $LOCAL 89 | cd ../ 90 | PWD=$(pwd) 91 | 92 | # Logging the call 93 | printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> $AR_LOG 94 | 95 | # Check for arguments 96 | check_args $# 97 | 98 | # Getting full alert 99 | ALERT=$(get_alert "$ALERTID") || { printf "Log for $ALERTID not found\n" && exit 1; } 100 | # Getting user 101 | USER=$(get_user "$ALERT") || { printf "User not found in $ALERTID\n" && exit 1; } 102 | # Check if system user and exit if true 103 | check_cdb "$USER" && { printf "User $USER found in $CDB\n" && exit 1; } 104 | # Getting source IP 105 | [[ "$IP" == - ]] && IP=$(get_ip "$ALERT") 106 | # Get hostname/ip for logs 107 | [[ "$AGENT" == - ]] && HOST=$(get_hostname "$ALERT") || HOST="$AGENT" 108 | 109 | MSG1="Employee lookup for attempted user" 110 | MSG2="Host ${HOST}, Src IP ${IP:-(empty)}, Orig. Alert $ALERTID" 111 | ldap_lookup "$USER" "$LDAP_SERVER" "$OU" || die "WARNING: $MSG1 ${USER}: $MSG2" 112 | log "OK: $MSG1 ${USER}: $MSG2" 113 | exit 0 114 | -------------------------------------------------------------------------------- /active-response/makelists.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Generates CDB lists from intel files 3 | # Author: Jon Schipp 4 | 5 | LOCAL=$(dirname $0); 6 | cd $LOCAL 7 | cd ../ 8 | PWD=$(pwd) 9 | 10 | # Logging the call 11 | printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> ${PWD}/../logs/active-responses.log 12 | 13 | # Build CDB databases 14 | /var/ossec/bin/ossec-makelists -c /var/ossec/etc/ossec.conf 15 | -------------------------------------------------------------------------------- /active-response/puppetdb_lookup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Jon Schipp 3 | PUPPETDB_SOCKET=http://puppet-master:8080 4 | PRE="$(date +"%Y-%m-%dT%T.%6N%:z") $(hostname -s) $(basename $0):" 5 | LOG="../logs/puppetdb_lookup.log" 6 | AR_LOG="${PWD}/../logs/active-responses.log" 7 | ACTION=$1 8 | USER=$2 9 | IP=$3 10 | ALERTID=$4 11 | RULEID=$5 12 | AGENT="$6" 13 | SERVICE=$7 14 | FILENAME=$8 15 | 16 | LOCAL=$(dirname $0); 17 | cd $LOCAL 18 | cd ../ 19 | PWD=$(pwd) 20 | 21 | die(){ 22 | [[ -d $(dirname $LOG) ]] && printf "$PRE $*\n" | tee -a $LOG 23 | exit 1 24 | } 25 | 26 | log(){ 27 | [[ -d $(dirname $LOG) ]] && printf "$PRE $*\n" | tee -a $LOG 28 | exit 0 29 | } 30 | 31 | check_args(){ 32 | local argc 33 | [[ $1 ]] || die "ERROR: ${FUNCNAME}: no argument given" 34 | argc="$1" 35 | [[ $argc -ge 5 ]] || return 1 36 | } 37 | 38 | get_alert(){ 39 | local id 40 | [[ $1 ]] || return 1 41 | id="$1" 42 | awk -v ts=${id}: 'BEGIN { RS=""; ORS="\n" } $0 ~ ts { print }' ${PWD}/../logs/alerts/alerts.log || return 1 43 | } 44 | 45 | get_host(){ 46 | awk -F "[()]" '/syscheck/ { print $2 }' | tail -n 1 47 | } 48 | 49 | get_file(){ 50 | awk -F "['']" '/^File|^New file|changed for:/ { print $2 }' 51 | } 52 | 53 | get_dns(){ 54 | local host 55 | local fqdn 56 | [[ $1 ]] || return 1 57 | host="$1" 58 | #Add domains to search below 59 | for name in $host ${host}.domain1.com ${host}.domain2.com 60 | do 61 | is_fqdn="$(dig $name +short)" 62 | [[ "$is_fqdn" ]] && fqdn="$name $fqdn" 63 | done 64 | if [[ "$fqdn" ]]; then 65 | printf "${fqdn}\n" 66 | else 67 | return 1 68 | fi 69 | } 70 | 71 | query_api_for_file(){ 72 | local file 73 | local name 74 | [[ $# -eq 2 ]] || die "ERROR: ${FUNCNAME}: Not enough arguments given" 75 | file="$1" 76 | name="$2" 77 | CURL="curl --connect-timeout 1 -X GET ${PUPPETDB_SOCKET}/v4/resources/File" 78 | JSON=$($CURL --data-urlencode 'query=["and",["=", "title", '\""$file"\"'],["=", "certname",'\""$name\""']]' 2>/dev/null) 79 | [[ "$JSON" ]] || die "ERROR: Something went wrong for API query, empty: ${dir}, $name" 80 | [[ "$JSON" =~ "$file" ]] || 81 | return 1 82 | } 83 | 84 | query_api_for_dir(){ 85 | local file 86 | local name 87 | [[ $# -eq 2 ]] || die "ERROR: ${FUNCNAME}: Not enough arguments given" 88 | file="$1" 89 | name="$2" 90 | dir=$(dirname $file) 91 | CURL="curl --connect-timeout 1 -X GET ${PUPPETDB_SOCKET}/v4/resources/File" 92 | JSON=$($CURL --data-urlencode 'query=["and",["=", "title", '\""$dir"\"'],["=", "certname",'\""$name\""']]' 2>/dev/null) 93 | [[ "$JSON" ]] || die "ERROR: Something went wrong for API query, empty: ${dir}, $name" 94 | [[ "$JSON" =~ "$dir" ]] && 95 | [[ "$JSON" =~ directory ]] && 96 | [[ "$JSON" =~ '"recurse" : true' ]] || 97 | return 1 98 | } 99 | 100 | check_status(){ 101 | local code 102 | [[ $1 ]] || die "ERROR: ${FUNCNAME}: No argument" 103 | code="$1" 104 | [[ $code -eq 0 ]] && log "OK: File $FILE managed by Puppet for $HOST_NAME" && return 0 105 | return 1 106 | } 107 | 108 | lookup_file(){ 109 | local file 110 | [[ $1 ]] || return 1 111 | file="$1" 112 | printf "File: ${file}\nHost: ${HOST_NAME}\n" 113 | for name in $DNS_NAMES 114 | do 115 | query_api_for_file "$file" "$name"; check_status $? && return 0 116 | query_api_for_dir "$file" "$name"; check_status $? && return 0 117 | done 118 | return 1 119 | } 120 | 121 | # Logging the call 122 | [[ -f "$AR_LOG" ]] && printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 ${8}\n" >> $AR_LOG 123 | 124 | # Getting full alert if we're passed arguments (by OSSEC), otherwise assume manual use 125 | if check_args $# 126 | then 127 | ALERT=$(get_alert $ALERTID) || die "ERROR: Retrieving alert failed" 128 | # Verify we have alert 129 | [[ "$ALERT" ]] || die "ERROR: Alert not found by timestamp" 130 | fi 131 | 132 | # Get information we need if not in environment 133 | if ! [[ "$HOST_NAME" ]]; then 134 | HOST_NAME=$(printf "${ALERT}\n" | get_host) || die "ERROR: Retrieving hostname failed" 135 | fi 136 | 137 | if ! [[ "$FILENAME" ]]; then 138 | FILENAME=$(printf "${ALERT}\n" | get_file) || die "ERROR: Retreiving file failed for $HOST_NAME" 139 | fi 140 | 141 | # Check for valid DNS names so we don't send more requests to puppet then we have to 142 | DNS_NAMES=$(get_dns $HOST_NAME) || die "ERROR: No matching FQDN found for $HOST_NAME" 143 | 144 | # Lookup file in PuppetDB 145 | lookup_file $FILENAME || die "WARNING: File ${FILENAME:-($ALERTID)} not found for $HOST_NAME" 146 | -------------------------------------------------------------------------------- /active-response/rpm_lookup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Jon Schipp 3 | PRE="$(date +"%Y-%m-%dT%T.%6N%:z") $(hostname -s) $(basename $0):" 4 | LOG="../logs/rpm_lookup.log" 5 | AR_LOG="${PWD}/../logs/active-responses.log" 6 | ALERTS_LOG="${PWD}/../logs/alerts/alerts.log" 7 | ACTION=$1 8 | USER=$2 9 | IP=$3 10 | ALERTID=$4 11 | RULEID=$5 12 | AGENT=$6 13 | [[ "$7" =~ syscheck ]] && FILENAME=$8 || FILENAME=$7 14 | 15 | die(){ 16 | printf "$PRE $*\n" | tee -a $LOG 17 | exit 1 18 | } 19 | 20 | log(){ 21 | printf "$PRE $*\n" | tee -a $LOG 22 | exit 0 23 | } 24 | 25 | file_type_lookup(){ 26 | local rpm_info 27 | rpm_info="$1" 28 | info=$(printf "$rpm_info" | awk '{ print $2 }') 29 | TYPE="Regular file" # Default to regular file, make changes after 30 | [[ "$info" == "$FILENAME" ]] && return 0 # True if $2 in rpm_info was empty (no file type info) 31 | [[ "$info" =~ c ]] && TYPE="Config file" 32 | [[ "$info" =~ d ]] && TYPE="Documentation file" 33 | [[ "$info" =~ g ]] && TYPE="GHost file" 34 | [[ "$info" =~ l ]] && TYPE="License file" 35 | [[ "$info" =~ r ]] && TYPE="Readme file" 36 | } 37 | 38 | file_status_lookup(){ 39 | local rpm_flags 40 | rpm_flags="$1" 41 | [[ "$rpm_flags" =~ missing ]] && STATUS="File is missing" 42 | [[ "$STATUS" ]] || STATUS="Size change" 43 | } 44 | 45 | rpm_lookup(){ 46 | local file 47 | file="$1" 48 | rpm_info=$(rpm -Vvf "$1" 2>/dev/null | fgrep -w "$1") 49 | [[ "$rpm_info" ]] || die "No flags output found for $file" 50 | file_type_lookup "$rpm_info" 51 | rpm_flags="$(printf "$rpm_info" | cut -d " " -f1)" 52 | [[ "$rpm_flags" ]] || die "No flags found for $file" 53 | file_status_lookup "$rpm_flags" 54 | [[ "$rpm_flags" =~ S|5|missing ]] || return 0 55 | return 1 56 | } 57 | 58 | LOCAL=$(dirname $0); 59 | cd $LOCAL 60 | cd ../ 61 | PWD=$(pwd) 62 | 63 | # Logging the call 64 | printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> $AR_LOG 65 | # System must be Red Hat based 66 | [[ -f /etc/redhat-release ]] || exit 0 67 | 68 | # We need a filename 69 | [[ "$FILENAME" ]] || die "No file given" 70 | 71 | rpm_lookup "$FILENAME" || die "WARNING: ${FILENAME:-(empty)} ($TYPE) differs from RPM database ($STATUS)" 72 | log "OK: ${FILENAME:-(empty)} ($TYPE) RPM verification passed" 73 | exit 0 74 | -------------------------------------------------------------------------------- /active-response/rule-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Jon Schipp 3 | CHAT=/usr/local/bin/ircsay 4 | PROG=OSSEC 5 | SCRIPT=$0 6 | CHANNEL="#ossec-alerts" 7 | MAIL=user@org 8 | ACTION=$1 9 | USER=$2 10 | IP=$3 11 | ALERTID=$4 12 | RULEID=$5 13 | 14 | [[ "$*" =~ syscheck ]] && exit 15 | 16 | # This scripts calls others because only one can be executed by OSSEC 17 | 18 | die(){ 19 | if [ -f ${COWSAY:-none} ]; then 20 | $COWSAY -d "$*" 21 | else 22 | printf "$*\n" 23 | fi 24 | exit 0 25 | } 26 | 27 | is_ip(){ 28 | [[ $IP ]] || return 1 29 | [[ $IP == '-' ]] && return 1 30 | return 0 31 | } 32 | 33 | LOCAL=$(dirname $0); 34 | cd $LOCAL 35 | cd ../ 36 | PWD=$(pwd) 37 | CHAT="$PWD/bin/alert2chat.sh" 38 | CIF="$PWD/bin/cif.sh" 39 | BHR="$PWD/bin/bhr.sh" 40 | CDB="$PWD/bin/add_to_cdb.sh" 41 | CMDS="$PWD/bin/command_search.sh" 42 | TS="$PWD/bin/time_lookup.sh" 43 | LDAP="$PWD/bin/ldap_lookup.sh" 44 | 45 | printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> ${PWD}/../logs/active-responses.log 46 | 47 | # Chat 48 | [[ -x $CHAT ]] && $CHAT $1 $2 $3 $4 $5 49 | 50 | # Collect system user accounts from 'new user' events, only work on rule 5902 51 | [[ -x $CDB ]] && [[ $RULEID -eq 5902 ]] && $CDB $1 $2 $3 $4 $5 52 | 53 | # Search for suspicious commands 54 | [[ -x $CMDS ]] && $CMDS $1 $2 $3 $4 $5 55 | 56 | # Check if system's clock is off 57 | [[ -x $TS ]] && $TS $1 $2 $3 $4 $5 58 | 59 | # Lookup user's in LDAP 60 | [[ -x $LDAP ]] && $LDAP $1 $2 $3 $4 $5 $6 61 | 62 | # CIF Feed 63 | is_ip && [[ -x $CIF ]] && $CIF $1 $2 $3 $4 $5 64 | 65 | # BHR Block 66 | is_ip && [[ -x $BHR ]] && $BHR $1 $2 $3 $4 $5 67 | -------------------------------------------------------------------------------- /active-response/syscheck-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Jon Schipp 3 | SCRIPT=$0 4 | ACTION=$1 5 | USER=$2 6 | IP=$3 7 | ALERTID=$4 8 | RULEID=$5 9 | AGENT=$6 10 | SERVICE=$7 11 | FILENAME=$8 12 | 13 | # This scripts calls others because only one can be executed by OSSEC 14 | 15 | # Match syscheck rules 16 | [[ "$*" =~ syscheck ]] || exit 17 | [[ $RULEID =~ ^(51[069]|55[0-5]|580|59[5-8])$ ]] || exit 18 | 19 | die(){ 20 | if [ -f ${COWSAY:-none} ]; then 21 | $COWSAY -d "$*" 22 | else 23 | printf "$*\n" 24 | fi 25 | exit 0 26 | } 27 | 28 | LOCAL=$(dirname $0); 29 | cd $LOCAL 30 | cd ../ 31 | PWD=$(pwd) 32 | PUPPET="$PWD/bin/puppetdb_lookup.sh" 33 | CHAT="$PWD/bin/syscheck2chat.sh" 34 | CYMRU="$PWD/bin/cymru_lookup.sh" 35 | VT="$PWD/bin/virustotal_lookup.sh" 36 | 37 | printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> ${PWD}/../logs/active-responses.log 38 | 39 | # Puppet 40 | [[ -x $PUPPET ]] && $PUPPET $1 $2 $3 $4 $5 $6 $7 $8; found="$?" 41 | [[ $found ]] && [[ $found -eq 0 ]] && exit 0 # If we made it here, file is managed by puppet and we no longer care 42 | 43 | # Chat 44 | [[ -x $CHAT ]] && $CHAT $1 $2 $3 $4 $5 $6 $7 $8 45 | 46 | # Cymru Hash Lookup 47 | [[ -x $CYMRU ]] && $CYMRU $1 $2 $3 $4 $5 $6 $7 $8 48 | 49 | # Virus Total Hash Lookup 50 | [[ -x $VT ]] && $VT $1 $2 $3 $4 $5 $6 $7 $8 51 | -------------------------------------------------------------------------------- /active-response/syscheck2chat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Author: Jon Schipp 3 | 4 | CHAT=/usr/local/bin/ircsay 5 | CHANNEL="#ossec-syscheck" 6 | ACTION=$1 7 | USER=$2 8 | IP=$3 9 | ALERTID=$4 10 | RULEID=$5 11 | 12 | # Check for chat program 13 | [[ -f $CHAT ]] || exit 1 14 | 15 | # Match syscheck rules 16 | [[ "$*" =~ syscheck ]] || exit 17 | [[ $RULEID =~ ^(51[069]|55[0-5]|580|59[5-8])$ ]] || exit 18 | 19 | LOCAL=$(dirname $0); 20 | cd $LOCAL 21 | cd ../ 22 | PWD=$(pwd) 23 | 24 | # Logging the call 25 | printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> ${PWD}/../logs/active-responses.log 26 | 27 | # Getting full alert 28 | awk -v ts=$ALERTID 'BEGIN { RS=""; ORS="\n" } $0 ~ ts { print }' ${PWD}/../logs/alerts/alerts.log | $CHAT "$CHANNEL" - 29 | -------------------------------------------------------------------------------- /active-response/time_lookup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Jon Schipp 3 | PRE="$(date +"%Y-%m-%dT%T.%6N%:z") $(hostname -s) $(basename $0):" 4 | LOG="../logs/time_lookup.log" 5 | AR_LOG="${PWD}/../logs/active-responses.log" 6 | ACTION=$1 7 | USER=$2 8 | IP=$3 9 | ALERTID=$4 10 | RULEID=$5 11 | AGENT=$6 12 | SERVICE=$7 13 | FILENAME=$8 14 | MAX_TIME=300 # 5 min in seconds 15 | 16 | die(){ 17 | printf "$PRE $*\n" | tee -a -- "$LOG" 18 | exit 1 19 | } 20 | 21 | log(){ 22 | printf "$PRE $*\n" >> "$LOG" 23 | exit 0 24 | } 25 | 26 | check_args(){ 27 | local argc 28 | argc="$1" 29 | [[ $argc -ge 5 ]] || die "ERROR: Not enough arguments given" 30 | } 31 | 32 | get_alert(){ 33 | local alert 34 | local alertid="$1" 35 | alert=$(awk -v ts=${alertid}: 'BEGIN { RS=""; ORS="\n" } $0 ~ ts { print }' ${PWD}/../logs/alerts/alerts.log) 36 | [[ "$alert" ]] || return 1 37 | printf "$alert\n" 38 | } 39 | 40 | convert_timestamp(){ 41 | local ts="$@" 42 | ts=$(date --date="$ts" +%s) 43 | [[ "$ts" ]] || return 1 44 | if [[ ${#ts} -eq 10 ]]; then 45 | printf "$ts\n" 46 | else 47 | return 1 48 | fi 49 | } 50 | 51 | get_timestamp(){ 52 | local alert="$@" 53 | 54 | # Attempt to get timestamp from log by format 55 | ts=$(printf "$alert\n" | grep -o '^20[1-9]\{2\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}\.[0-9]\{6\}') 56 | if [[ "$ts" ]]; then 57 | epoch=$(convert_timestamp "$ts") 58 | if [[ "$epoch" ]]; then 59 | printf "$epoch" 60 | return 0 61 | fi 62 | fi 63 | 64 | ts=$(printf "$alert\n" | grep -o '^[A-Z][a-z]\+ [0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}') 65 | if [[ "$ts" ]]; then 66 | epoch=$(convert_timestamp "$ts") 67 | if [[ "$epoch" ]]; then 68 | printf "$epoch" 69 | return 0 70 | fi 71 | fi 72 | # Add more patterns to extract your logs timestamp if they differ 73 | 74 | # If we made it here none of the timestamps matched 75 | return 1 76 | } 77 | 78 | get_utc_timestamp(){ 79 | local alert="$@" 80 | local ossec_ts 81 | local year 82 | local datetime 83 | local ossec_ts 84 | local ossec_epoch 85 | # Attempt to get OSSEC alert timestamp (UTC) 86 | ossec_ts=$(printf "$alert\n" | grep -o '^20[1-9]\{2\} [A-Z][a-z]\+ [0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}') 87 | if [[ "$ossec_ts" ]]; then 88 | # date: invalid date `2016 Feb 25 00:00:17' 89 | # Format so year is at end 90 | year="${ossec_ts%% *}" 91 | datetime="${ossec_ts#* }" 92 | ossec_ts="${datetime} ${year}" 93 | ossec_epoch=$(convert_timestamp "$ossec_ts") || return 1 94 | fi 95 | printf "$ossec_epoch\n" 96 | } 97 | 98 | get_hostname(){ 99 | local alert="$@" 100 | local host 101 | host=$(printf "$alert\n" | awk '/^2016 [A-Z][a-z][a-z] [0-9][0-9]/ { print $5 }') 102 | printf "$host\n" 103 | } 104 | 105 | check_existing_entry(){ 106 | local host="$1" 107 | fgrep -q "$host" $LOG && return 1 108 | return 0 109 | } 110 | 111 | is_utc(){ 112 | local ossec_ts="$1" 113 | local log_ts="$2" 114 | local tz=0 115 | utc=$((log_ts-3600*5)) 116 | dst1=$((log_ts-3600*6)) 117 | dst2=$((log_ts-3600*7)) 118 | for ts in $utc $dst1 $dst2; do 119 | result=$((ossec_ts-ts)) 120 | [[ $result -eq 18000 ]] && tz=1 121 | [[ $result -eq 21600 ]] && tz=1 122 | [[ $result -eq 25200 ]] && tz=1 123 | done 124 | return $tz 125 | } 126 | 127 | check_timestamp(){ 128 | local log_ts="$1" 129 | local host="$2" 130 | local current=$(date +"%s") 131 | seconds="$((current-log_ts))" 132 | [[ $seconds -lt 0 ]] && die "WARNING: system clock is ahead by $seconds seconds for $host on ${ALERTID}, ${current}-${log_ts}" 133 | [[ $seconds -lt $MAX_TIME ]] || die "WARNING: system clock is behind by $seconds seconds for $host on ${ALERTID}, ${current}-${log_ts}" 134 | } 135 | 136 | # Check for arguments 137 | check_args $# 138 | 139 | LOCAL=$(dirname $0); 140 | cd $LOCAL 141 | cd ../ 142 | PWD=$(pwd) 143 | 144 | # Logging the call 145 | [[ -f "$AR_LOG" ]] && printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> $AR_LOG 146 | 147 | # Getting full alert 148 | ALERT=$(get_alert "$ALERTID") || exit 149 | # Skip if alert has multiple logs 150 | [[ "$ALERT" =~ [Mm]ultiple ]] && exit 151 | # Get timestamp 152 | TS=$(get_timestamp "$ALERT") || die "ERROR: get_timestamp: No timestamp found for $ALERTID" 153 | # Get hostname for logs 154 | HOST=$(get_hostname "$ALERT") 155 | # Don't run on ossec logs 156 | [[ $HOST =~ ossec ]] && exit 157 | 158 | # Skip writing warning logs if there's an entry for this host already 159 | # This will cut down on alerts 160 | check_existing_entry "$HOST" || exit 1 161 | 162 | UTC_EPOCH=$(get_utc_timestamp "$ALERT") || die "ERROR: get_utc_timestamp: utc epoch not found in alert" 163 | is_utc "$UTC_EPOCH" "$TS" || die "WARNING: System clock for $HOST is not localtime on ${ALERTID}, Alert_UTC:${UTC_EPOCH} Log_Local:${TS}" 164 | check_timestamp "$TS" "$HOST" 165 | -------------------------------------------------------------------------------- /active-response/virus_total.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import json 3 | import urllib 4 | import urllib2 5 | import sys 6 | 7 | apikey = '' 8 | 9 | def usage(): 10 | print '''Submit hash to virtus-total 11 | (Place your VirusTotal apikey in this script) 12 | Usage: %s ''' % sys.argv[0] 13 | exit(1) 14 | 15 | def collect(data): 16 | retrieve = data[0] 17 | sha1 = retrieve['sha1'] 18 | filenames = retrieve['filenames'] 19 | first_seen = retrieve['first-seen'] 20 | last_seen = retrieve['last-seen'] 21 | last_scan_permalink = retrieve['last-scan-permalink'] 22 | last_scan_report = retrieve['last-scan-report'] 23 | return sha1, filenames, first_seen, last_seen, last_scan_permalink, last_scan_report 24 | 25 | def msg(sha1, filenames, first_seen, last_seen, last_scan_permalink): 26 | print '''===Suspected Malware Item=== 27 | SHA1: %s 28 | Filenames: %s 29 | First Seen: %s 30 | Last Seen: %s 31 | Link: %s''' % (sha1, filenames, first_seen, last_seen, last_scan_permalink) 32 | 33 | def is_malware(last_scan_report): 34 | for av, scan in last_scan_report.iteritems(): 35 | if scan[0] is not None: 36 | return True 37 | return False 38 | 39 | def in_database(data, mhash): 40 | result = data[0]['result'] 41 | if result == 0: 42 | return False 43 | return True 44 | 45 | def arguments(): 46 | if len(sys.argv) < 2: 47 | usage() 48 | if '-h' in sys.argv[1]: 49 | usage() 50 | if not apikey: 51 | print "Set apikey in %s to value of your Virus Total key" % sys.argv[0] 52 | exit(1) 53 | 54 | mhash = sys.argv[1] 55 | return mhash 56 | 57 | def query_api(mhash, apikey): 58 | url = "http://api.vtapi.net/vtapi/get_file_infos.json" 59 | parameters = {"resources": mhash, "apikey": apikey} 60 | encoded = urllib.urlencode(parameters) 61 | req = urllib2.Request(url, encoded) 62 | response = urllib2.urlopen(req) 63 | response_string = response.read() 64 | data = json.loads(response_string) 65 | return data 66 | 67 | mhash = arguments() 68 | data = query_api(mhash, apikey) 69 | 70 | if not in_database(data, mhash): 71 | print 'No entry for %s in database' % mhash 72 | exit(1) 73 | 74 | # Positive match found 75 | sha1, filenames, first_seen, last_seen, last_scan_permalink, last_scan_report = collect(data) 76 | if is_malware(last_scan_report): 77 | msg(sha1, filenames, first_seen, last_seen, last_scan_permalink) 78 | exit(0) 79 | else: 80 | print 'Entry %s is not malicious' % mhash 81 | exit(1) 82 | -------------------------------------------------------------------------------- /active-response/virustotal_lookup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Jon Schipp 3 | PRE="$(date +"%Y-%m-%dT%T.%6N%:z") $(hostname -s) $(basename $0):" 4 | LOG="../logs/virustotal_lookup.log" 5 | AR_LOG="${PWD}/../logs/active-responses.log" 6 | CDB="/var/ossec/lists/hashes.list" 7 | ACTION=$1 8 | USER=$2 9 | IP=$3 10 | ALERTID=$4 11 | RULEID=$5 12 | AGENT=$6 13 | SERVICE=$7 14 | FILENAME=$8 15 | 16 | die(){ 17 | [[ -d $(dirname $LOG) ]] && printf "$PRE $*\n" | tee -a $LOG 18 | exit 1 19 | } 20 | 21 | log(){ 22 | [[ -d $(dirname $LOG) ]] && printf "$PRE $*\n" | tee -a $LOG 23 | exit 0 24 | } 25 | 26 | check_args(){ 27 | local argc 28 | argc="$1" 29 | [[ $argc -ge 5 ]] || die "ERROR: Not enough arguments given" 30 | } 31 | 32 | 33 | get_alert(){ 34 | local alert 35 | alert=$(awk -v ts=${ALERTID}: 'BEGIN { RS=""; ORS="\n" } $0 ~ ts { print }' ${PWD}/../logs/alerts/alerts.log) 36 | [[ "$alert" ]] || return 1 37 | printf "$alert\n" 38 | } 39 | 40 | is_hash(){ 41 | local sha1 42 | sha1="$1" 43 | [[ $sha1 ]] || die "ERROR: ${FUNCNAME}: Hash variable empty" 44 | [[ $sha1 =~ ^\ +$ ]] && die "ERROR: ${FUNCNAME}: Hash variable is only whitespace" 45 | len="${#sha1}"; [[ $len -eq 40 ]] || die "ERROR: ${FUNCNAME}: The Hash variable does not contain a SHA1 hash" 46 | } 47 | 48 | get_hash_from_alert(){ 49 | local alert 50 | local sha1 51 | alert="$@" 52 | [[ "$FILENAME" ]] || FILENAME=$(printf "$alert\n" | awk -F "['']" '/^File|changed for:/ { print $2 }') 53 | sha1=$(printf "$alert\n" | grep -o '[a-zA-Z0-9]\{40\}' | tail -n 1) 54 | printf "$sha1\n" 55 | } 56 | 57 | get_hash_from_filename(){ 58 | local alert 59 | local sha1 60 | alert="$@" 61 | [[ "$FILENAME" ]] || FILENAME=$(printf "$alert\n" | awk -F "['']" '/^File|changed for:/ { print $2 }') 62 | [[ -r "$FILENAME" ]] || die "ERROR: ${FUNCNAME}: File not available on system" 63 | sha1=$(sha1sum $file | grep -o '[a-zA-Z0-9]\{40\}') 64 | printf "$sha1\n" 65 | } 66 | 67 | virus_lookup(){ 68 | local sha1 69 | local results 70 | sha1="$1" 71 | results=$($PWD/bin/virus_total.py $sha1) 72 | status_code=$? 73 | printf "$results\n" 74 | return $status_code 75 | } 76 | 77 | is_executable(){ 78 | local file 79 | file="$1" 80 | file -L "$file" 2>/dev/null | egrep -i -q 'script|exec|ELF|object|stripped|linked' || return 1 81 | return 0 82 | } 83 | 84 | send_cdb(){ 85 | local checksum 86 | local file 87 | checksum="$1" 88 | file="$2" 89 | if [[ -w "$CDB" ]]; then 90 | printf "${checksum}:${file:-empty}\n" >> $CDB 91 | fi 92 | } 93 | 94 | check_cdb(){ 95 | local checksum 96 | checksum="$1" 97 | if [[ -r "$CDB" ]]; then 98 | grep -q "$checksum" $CDB && exit 0 99 | fi 100 | } 101 | 102 | # Check for arguments 103 | check_args $# 104 | 105 | LOCAL=$(dirname $0); 106 | cd $LOCAL 107 | cd ../ 108 | PWD=$(pwd) 109 | 110 | # Logging the call 111 | [[ -f "$AR_LOG" ]] && printf "$(date) $0 $1 $2 $3 $4 $5 $6 $7 $8\n" >> $AR_LOG 112 | 113 | # Getting full alert 114 | ALERT=$(get_alert) || die "ERROR: get_alert: No alert found matching timestamp $ALERTID" 115 | 116 | # Obtain hash 117 | [[ $RULEID -eq 554 ]] && HASH=$(get_hash_from_filename "$ALERT") || HASH=$(get_hash_from_alert "$ALERT") 118 | # Grab host for logging 119 | [[ "$AGENT" ]] || AGENT=$(printf "$ALERT\n" | awk -F "[()]" '/syscheck/ { print $2 }' | tail -n 1) 120 | 121 | # Only perform lookups for executable files 122 | is_executable "$FILENAME" || die "OK: ${FILENAME:-(empty)} is not an executable file" 123 | 124 | # Verify we do indeed have a hash 125 | is_hash "$HASH" 126 | 127 | # Check if we've looked up this hash previously 128 | check_cdb "$HASH" 129 | 130 | # Lookup 131 | RESULTS=$(virus_lookup $HASH) || { send_cdb "$HASH" "$FILENAME" && die "OK: No malware found for ${FILENAME:-(empty)} ${HASH:-(empty)} on ${AGENT:-(empty)}"; } 132 | 133 | # Log 134 | log "Malicious hash found for ${FILENAME:-(empty)} ($HASH) on ${AGENT:-(empty)}" 135 | -------------------------------------------------------------------------------- /decoders/active_response.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | ^rpm_lookup.sh$ 9 | 10 | 11 | 12 | rpm_lookup 13 | ^(\S+): (\S+) 14 | status, id 15 | 16 | 17 | 23 | 24 | ^deb_lookup.sh$ 25 | 26 | 27 | 28 | deb_lookup 29 | ^(\S+): (\S+) 30 | status, id 31 | 32 | 33 | 39 | 40 | ^puppetdb_lookup.sh$ 41 | 42 | 43 | 44 | puppetdb_lookup 45 | ^(\S+): File (\S+) 46 | status, id 47 | 48 | 49 | 56 | 57 | ^cymru_lookup.sh$ 58 | 59 | 60 | 61 | cymru_lookup 62 | ^(\S+): No match found for \S+ \((\S+)\) 63 | status, id 64 | 65 | 66 | 67 | cymru_lookup 68 | ^(\S+): Malicious hash found for \S+ \((\S+)\) 69 | status, id 70 | 71 | 72 | 79 | 80 | ^ldap_lookup.sh$ 81 | 82 | 83 | 84 | ldap_lookup 85 | ^(\S+): NCSA employee lookup for attempted user (\w+): Host (\d+.\d+.\d+.\d+), Src IP (\d+.\d+.\d+.\d+), 86 | status, user, dstip, srcip 87 | 88 | -------------------------------------------------------------------------------- /decoders/kerberos_decoder.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | ^ksu 14 | 15 | 16 | 17 | kerberos_ksu 18 | ^'ksu \S+' authentication failed 19 | ^'ksu (\S+)' authentication failed for (\S+) on \S+$ 20 | dstuser, id 21 | 22 | 23 | 24 | kerberos_ksu 25 | ^'ksu \S+' authenticated 26 | ^'ksu (\S+)' authenticated (\S+) for (\S+) on \S+$ 27 | dstuser, id, extra_data 28 | 29 | 30 | 31 | kerberos_ksu 32 | ^Account \S+: authorization for \S+ successful 33 | ^Account (\S+): authorization for (\S+) successful$ 34 | dstuser, id 35 | 36 | 37 | 38 | kerberos_ksu 39 | ^Account \S+: authorization for \S+ for execution of \S+ successful 40 | ^Account (\S+): authorization for (\S+) for execution of (\S+) successful$ 41 | dstuser, id, extra_data 42 | 43 | 44 | 53 | 54 | 55 | ^kadmind 56 | 57 | 58 | 59 | kerberos_kadmin 60 | ^Request: kadm5_create_principal, 61 | ^Request: kadm5_create_principal, (\S+), (\S+), client=(\S+), service=\S+, addr=(\S+)$ 62 | dstuser, status, id, srcip 63 | 64 | 65 | 66 | kerberos_kadmin 67 | ^Request: kadm5_get_principal, 68 | ^Request: kadm5_get_principal, (\S+), (\S+), client=(\S+), service=\S+, addr=(\S+)$| 69 | ^Request: kadm5_get_principal, (\S+), (Principal does not exist), client=(\S+), service=\S+, addr=(\S+)$ 70 | dstuser, status, id, srcip 71 | 72 | 73 | 74 | kerberos_kadmin 75 | ^Request: kadm5_modify_principal, 76 | ^Request: kadm5_modify_principal, (\S+), (\S+), client=(\S+), service=\S+, addr=(\S+)$| 77 | ^Request: kadm5_modify_principal, (\S+), (Principal does not exist), client=(\S+), service=\S+, addr=(\S+)$ 78 | dstuser, status, id, srcip 79 | 80 | -------------------------------------------------------------------------------- /decoders/sudo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 33 | 34 | ^sudo 35 | ^\s+(\S+)\s:\sTTY=\S+\s;\sPWD=(\S+)\s;\sUSER=(\S+)\s;\sCOMMAND=(\.+)$ 36 | id,url,dstuser,status 37 | name,user,location 38 | First time user executed the sudo command 39 | 40 | -------------------------------------------------------------------------------- /munin/ossec_ar_stats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | FILE=/var/ossec/logs/active-responses.log 3 | DIR=/var/ossec/active-response/bin 4 | config(){ 5 | cat <= crit: 76 | return NAGIOS_CRITICAL 77 | if inactive_agents >= warn: 78 | return NAGIOS_WARNING 79 | return NAGIOS_OK 80 | 81 | def is_ossec(path): 82 | if os.path.isdir(path): 83 | files = [ 84 | 'etc/ossec.conf', 85 | 'etc/shared/agent.conf', 86 | 'bin/syscheck_control', 87 | 'bin/rootcheck_control', 88 | 'bin/agent_control' 89 | ] 90 | for f in files: 91 | fp = path + '/' + f 92 | if not os.path.isfile(fp): 93 | print "Error: Installation missing file %s" % fp 94 | return False 95 | return True 96 | 97 | def get_output_dict(cmd, arg): 98 | c=0 99 | data={} 100 | command = path + '/' + cmd 101 | result = subprocess.Popen([command,arg],stdout=subprocess.PIPE,stderr=subprocess.PIPE) 102 | set_result = set(result.stdout) 103 | for i in set_result: 104 | data[c]= [i.split(',')] 105 | c += 1 106 | return data 107 | 108 | def get_output_set(cmd, arg): 109 | command = path + '/' + cmd 110 | result = subprocess.Popen([command,arg],stdout=subprocess.PIPE,stderr=subprocess.PIPE) 111 | data = set(result.stdout) 112 | return data 113 | 114 | def is_server(): 115 | filename = '/etc/ossec-init.conf' 116 | try: 117 | with open(filename, 'r') as f: 118 | contents = f.readlines() 119 | install_type = contents[3].strip().split('=')[1] 120 | if install_type == '"server"': 121 | return True 122 | else: 123 | print "ERROR: Unable to detect OSSEC server: %s" % install_type 124 | return False 125 | 126 | except IOError: 127 | print "ERROR: Cannot open file %s. Is OSSEC installed?" % filename 128 | exit(NAGIOS_UNKNOWN) 129 | 130 | def open_queue(filename, service): 131 | try: 132 | with open(filename, 'r') as f: 133 | for i in f.readlines(): 134 | if service == 'syscheck': 135 | if 'Starting syscheck scan.' in i: 136 | syscheck_start_ts = i[1:11] 137 | return syscheck_start_ts 138 | if service == 'rootcheck': 139 | if 'Starting rootcheck scan.' in i: 140 | rootcheck_start_ts = i[1:11] 141 | return rootcheck_start_ts 142 | else: 143 | return False 144 | except IOError: 145 | print "ERROR: Cannot read queue file %s" % filename 146 | exit(NAGIOS_UNKNOWN) 147 | 148 | def older_than(ts, name, crit_ts, warn_ts): 149 | if not ts: 150 | print "%s: %s: LastCheckTime: Unknown" % (STATUS_MSG[NAGIOS_UNKNOWN], name) 151 | return NAGIOS_UNKNOWN 152 | service_timestamp = datetime.datetime.fromtimestamp(float(ts)) 153 | if service_timestamp < crit_ts: 154 | print "%s: %s: LastCheckTime: %s" % (STATUS_MSG[NAGIOS_CRITICAL], name, service_timestamp) 155 | return NAGIOS_CRITICAL 156 | elif service_timestamp < warn_ts: 157 | print "%s: %s: LastCheckTime: %s" % (STATUS_MSG[NAGIOS_WARNING], name, service_timestamp) 158 | return NAGIOS_WARNING 159 | else: 160 | return NAGIOS_OK 161 | 162 | def check_connected(agents, skip, crit, warn): 163 | data = get_output_dict('bin/agent_control', '-l') 164 | c=0 165 | inactive_agents = 0 166 | active = [ 'Active', 'Active/Local' ] 167 | notactive = {} 168 | 169 | for i in data: 170 | line = data[c][0] 171 | c += 1 172 | # Check for lines with fields we need 173 | # Extract agent name and status message 174 | if len(line) == 4: 175 | name=line[1][6:].lstrip() 176 | status=line[3].lstrip().rstrip() 177 | 178 | # If --agents is specified only check for specified agents 179 | if agents: 180 | if name not in agents: 181 | continue 182 | else: 183 | # If --agents is not specified check all except in skip 184 | if name in skip: 185 | continue 186 | 187 | if status not in active: 188 | notactive[name] = status 189 | inactive_agents += 1 190 | 191 | # Check if inactive agents were fonud 192 | if notactive: 193 | exit_code = threshold(inactive_agents, crit, warn) 194 | print "%s: %d agents not connected" % (STATUS_MSG[exit_code], len(notactive)) 195 | for k,v in notactive.items(): 196 | print "Name: %s, Status: %s" % (k,v) 197 | return exit_code 198 | else: 199 | print "%s: All agents connected" % STATUS_MSG[NAGIOS_OK] 200 | return NAGIOS_OK 201 | 202 | def check_service(service, agents, skip, crit, warn): 203 | dir = os.path.join(path,'queue/rootcheck') 204 | crit_ts = datetime.datetime.now() - datetime.timedelta(hours=crit) 205 | warn_ts = datetime.datetime.now() - datetime.timedelta(hours=warn) 206 | status_list = [] 207 | for queue in os.listdir(dir): 208 | name = queue.strip('()').split()[0].rstrip(')') 209 | if agents: 210 | if name not in agents: 211 | continue 212 | else: 213 | if name == 'rootcheck': 214 | continue 215 | if name in skip: 216 | continue 217 | f = os.path.join(dir, queue) 218 | if os.path.isfile(f): 219 | ts = open_queue(f, service) 220 | status = older_than(ts, name, crit_ts, warn_ts) 221 | status_list.append(status) 222 | exit_code = max(status_list) 223 | if exit_code == NAGIOS_OK: 224 | print "%s: Agent %s runtimes are up to date" % (STATUS_MSG[NAGIOS_OK],service) 225 | return exit_code 226 | 227 | def check_status(agents, skip): 228 | data = get_output_set('bin/ossec-control', 'status') 229 | not_running=[] 230 | for i in data: 231 | # If --skip is used skip these 232 | if i in skip: 233 | continue 234 | # Add names of services not running to list 235 | if 'not running' in i: 236 | not_running.append(i) 237 | continue 238 | # Test for entries in list. Entries mean something wasn't running 239 | if not_running: 240 | print "%s: Some services running" % STATUS_MSG[NAGIOS_CRITICAL] 241 | for i in not_running: 242 | print i.rstrip() 243 | return NAGIOS_CRITICAL 244 | else: 245 | print "%s: All services running" % STATUS_MSG[NAGIOS_OK] 246 | return NAGIOS_OK 247 | 248 | def main(): 249 | option, agents, skip, crit, warn = arguments() 250 | 251 | if not is_ossec(path): 252 | exit(NAGIOS_UNKNOWN) 253 | if not is_server(): 254 | exit(NAGIOS_UNKNOWN) 255 | 256 | if option == "connected": 257 | exit(check_connected(agents, skip, crit, warn)) 258 | elif option == "syscheck": 259 | exit(check_service(option, agents, skip, crit, warn)) 260 | elif option == "rootcheck": 261 | exit(check_service(option, agents, skip, crit, warn)) 262 | elif option == "status": 263 | exit(check_status(agents, skip)) 264 | else: 265 | print "Invalid type option" 266 | exit(NAGIOS_UNKNOWN) 267 | 268 | if __name__ == "__main__": 269 | main() 270 | -------------------------------------------------------------------------------- /nagios/check_ossec.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Author: Jon Schipp 4 | # Date: 11-07-2013 5 | ######## 6 | # Examples: 7 | 8 | # 1.) Check status of OSSEC services excluding active response i.e. execd 9 | # $ ./check_ossec.sh -s execd 10 | 11 | # 2.) Check status of OSSEC agent 12 | # $ ./check_ossec.sh -a server1 13 | 14 | # 3.) Check status of multiple OSSEC agents 15 | # $ ./check_ossec.sh -a "server1,server2,station3" 16 | 17 | # 4.) Report critical if more than 3 agents are offline and warning if at least 1 is offline. 18 | # $ ./check_ossec.sh -c 3 -w 1 19 | 20 | # Nagios Exit Codes 21 | OK=0 22 | WARNING=1 23 | CRITICAL=2 24 | UNKNOWN=3 25 | 26 | # Default Location of the binaries 27 | # Set these to the proper location if your installation differs 28 | CONF=/var/ossec/etc/ossec.conf 29 | AGENT_CONF=/var/ossec/etc/shared/agent.conf 30 | AGENT_CONTROL=/var/ossec/bin/agent_control 31 | OSSEC_CONTROL=/var/ossec/bin/ossec-control 32 | SYSCHECK_CONTROL=/var/ossec/bin/syscheck-control 33 | ROOTKIT_CONTROL=/var/ossec/bin/rootkit-control 34 | 35 | if [ ! -f $AGENT_CONTROL ] && [ ! -f $OSSEC_CONTROL ]; 36 | then 37 | echo "ERROR: OSSEC binaries not found. If you installed OSSEC in a location other than the default update the AGENT_CONTROL and OSSEC_CONTROL variables in $0." 38 | exit 1 39 | fi 40 | 41 | usage() 42 | { 43 | cat < Check status of agent or list of comma separated agents, "agent1,agent2". 50 | -c Critical threshold for number of inactive agents 51 | -l List all agents 52 | -s Check status of OSSEC server processes. Use ``-s all'' to check all. 53 | To exclude a service e.g execd pass as argument i.e. ``-s execd'' 54 | -w Warning threshold for number of inactive agents 55 | -S Syscheck last scan check 56 | -R Rootkit last scan check 57 | 58 | Usage: $0 -a "server1,server2,station3" 59 | EOF 60 | } 61 | 62 | argcheck() { 63 | # if less than n argument 64 | if [ $ARGC -lt $1 ]; then 65 | echo "Missing arguments! Use \`\`-h'' for help." 66 | exit 1 67 | fi 68 | } 69 | 70 | # Initialize variables 71 | CRIT=0 72 | WARN=0 73 | CHECK_AGENT=0 74 | CHECK_THRESHOLD=0 75 | CHECK_SYSCHECK=0 76 | CHECK_ROOTCHECK=0 77 | LIST_AGENTS=0 78 | SERVER_CHECK=0 79 | EXCLUDE=all 80 | ACTIVE=0 81 | INACTIVE=0 82 | NEVER=0 83 | TOTAL=0 84 | DISCONNECTED=0 85 | CONNECTED=0 86 | NEVERCONNECTED=0 87 | UNKNOWN=0 88 | ARGC=$# 89 | 90 | argcheck 1 91 | 92 | while getopts "ha:c:ls:v:w:SR" OPTION 93 | do 94 | case $OPTION in 95 | h) 96 | usage;; 97 | a) 98 | CHECK_AGENT=1 99 | AGENT="$OPTARG" 100 | ;; 101 | c) 102 | CHECK_THRESHOLD=1 103 | CRIT="$OPTARG" 104 | ;; 105 | l) 106 | LIST_AGENTS=1 107 | ;; 108 | s) 109 | SERVER_CHECK=1 110 | EXCLUDE="$OPTARG" 111 | ;; 112 | v) 113 | CHECK_THRESHOLD=1 114 | VOL="$OPTARG" 115 | ;; 116 | w) 117 | WARN="$OPTARG";; 118 | S) 119 | CHECK_SYSCHECK=1;; 120 | R) 121 | CHECK_ROOTCHECK=1;; 122 | \?) 123 | exit 1;; 124 | esac 125 | done 126 | 127 | if [ $LIST_AGENTS -eq 1 ]; then 128 | $AGENT_CONTROL -l 129 | exit 0 130 | fi 131 | 132 | if [ $SERVER_CHECK -eq 1 ]; then 133 | 134 | $OSSEC_CONTROL status | grep -v $EXCLUDE | grep "not running" 135 | 136 | if [ $? -eq 0 ]; then 137 | echo "An OSSEC service is not running!" 138 | exit $CRITICAL 139 | else 140 | echo "All OSSEC services running" 141 | exit $OK 142 | fi 143 | fi 144 | 145 | if [ $CHECK_AGENT -eq 1 ]; then 146 | 147 | for host in $(echo $AGENT | sed 's/,/ /g'); 148 | do 149 | RESULT=$($AGENT_CONTROL -l | grep ${host},) 150 | case $RESULT in 151 | *Disconnected) 152 | echo "Agent $host is not connected!" 153 | DISCONNECTED=$((DISCONNECTED+1)) 154 | ;; 155 | *Active) 156 | echo "Agent $host is connected" 157 | CONNECTED=$((CONNECTED+1)) 158 | ;; 159 | *Never*) 160 | echo "Agent $host has never connected to the server: $RESULT" 161 | NEVERCONNECTED=$((NEVERCONNECTED+1)) 162 | ;; 163 | *) 164 | echo "Unknown status or agent: $host" 165 | UNKNOWN=$((UNKNOWN+1)) 166 | ;; 167 | esac 168 | done 169 | 170 | if [ $DISCONNECTED -gt 0 ] || [ $NEVERCONNECTED -gt 0 ] || [ $UNKNOWN -gt 0 ]; then 171 | echo "-> $DISCONNECTED disconnected agent(s), $NEVERCONNECTED never connected agent(s), and $UNKNOWN agent(s) with unknown status (possible agent name typo?)." 172 | exit $CRITICAL 173 | else 174 | echo "All requested ($CONNECTED) agents are connected to the server!" 175 | exit $OK 176 | fi 177 | fi 178 | 179 | 180 | if [ $CHECK_THRESHOLD -eq 1 ]; then 181 | 182 | ACTIVE=$($AGENT_CONTROL -l | grep Active | wc -l) 183 | INACTIVE=$($AGENT_CONTROL -l | grep Disconnected | wc -l) 184 | NEVER=$($AGENT_CONTROL -l | grep Never | wc -l) 185 | TOTAL=$($AGENT_CONTROL -l | wc -l) 186 | 187 | if [ $INACTIVE -gt $CRIT ]; then 188 | echo "$INACTIVE of $TOTAL agents inactive! Active: $ACTIVE" 189 | exit $CRITICAL 190 | elif [ $INACTIVE -gt $WARN ]; then 191 | echo "$INACTIVE of $TOTAL agents inactive! Active: $ACTIVE" 192 | exit $WARNING 193 | else 194 | echo "Active: $ACTIVE - Inactive: $INACTIVE - Never connected:$NEVER - Total: $TOTAL" 195 | exit $OK 196 | fi 197 | fi 198 | 199 | [[ '' ]] 200 | 201 | if [ $CHECK_SYSCHECK -eq 1 ]; then 202 | 203 | 204 | fi 205 | 206 | if [ $CHECK_ROOTCHECK -eq 1 ]; then 207 | fi 208 | -------------------------------------------------------------------------------- /rules/kerberos_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | kerberos_ksu 6 | Kerberos messages grouped. 7 | 8 | 9 | 10 | 11 | 12 | 15000 13 | kerberos_ksu 14 | authenticated 15 | ksu authentication succeeded. 16 | authentication_success, 17 | 18 | 19 | 20 | 15001 21 | root 22 | ksu root authentication succeeded. 23 | authentication_success, 24 | 25 | 26 | 27 | 28 | 29 | 15000 30 | kerberos_ksu 31 | authentication failed 32 | ksu authentication failed. 33 | authentication_failed, 34 | 35 | 36 | 37 | 15003 38 | root 39 | ksu authentication to root failed. 40 | authentication_failed, 41 | 42 | 43 | 44 | 45 | 46 | 15000 47 | kerberos_ksu 48 | authorization for \S+ successful 49 | ksu authorization succeeded. 50 | authentication_success, 51 | 52 | 53 | 54 | 15000 55 | kerberos_ksu 56 | authorization for \S+ for execution of \S+ successful 57 | ksu authorization succeeded to run command. 58 | authentication_success, 59 | 60 | 61 | 62 | 15005, 15006 63 | root 64 | ksu authorization to root succeeded. 65 | authentication_success, 66 | 67 | 68 | 69 | 15007 70 | 71 | alert_by_email 72 | First time (ksu) is executed by user. 73 | 74 | 75 | 76 | 77 | 78 | kerberos_kadmin 79 | Kerberos messages grouped. 80 | 81 | 82 | 83 | 84 | 85 | 15100 86 | create_principal 87 | success 88 | Kerberus principal created 89 | adduser,account_changed 90 | 91 | 92 | 93 | 15100 94 | get_principal 95 | success 96 | Kerberus principal successfully requested 97 | authentication_success, 98 | 99 | 100 | 101 | 15100 102 | modify_principal 103 | success 104 | Kerberus principal successfully modified 105 | adduser,account_changed 106 | 107 | 108 | 109 | 110 | 111 | 15100 112 | get_principal 113 | Principal does not exist 114 | Invalid Kerberus principal requested 115 | invalid_login 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /rules/organization_ar_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | virustotal_lookup.sh 7 | Malicious hash found 8 | Malware hash found in Virus Total 9 | 10 | 11 | 12 | cymru_lookup.sh 13 | WARNING 14 | Malware hash found in Team Cymru Malware Hash Registry 15 | 16 | 17 | 18 | 19 | rpm_lookup 20 | Custom RPM Lookup Alert 21 | rpm_lookup 22 | 23 | 24 | 25 | 120003 26 | rpm_lookup.sh 27 | WARNING 28 | File has been modified, entry in RPM database differs 29 | no_email_alert 30 | rpm_lookup 31 | 32 | 33 | 34 | puppetdb_lookup 35 | Custom PuppetDB Lookup Alert 36 | puppetdb_lookup 37 | 38 | 39 | 40 | 120005 41 | puppetdb_lookup.sh 42 | WARNING 43 | Modified file not managed by Puppet 44 | no_email_alert 45 | puppetdb_lookup 46 | 47 | 48 | 49 | 120006 50 | 120004 51 | 52 | File has been modified and is not in PuppetDB or RPM database 53 | 54 | 55 | 56 | cymru_lookup 57 | Custom Cymru Lookup Alert 58 | cymru_lookup 59 | 60 | 61 | 62 | 120008 63 | lists/mal.list 64 | Malware hash match found from intelligence data 65 | 66 | 67 | 68 | deb_lookup 69 | Custom DEB Lookup Alert 70 | deb_lookup 71 | 72 | 73 | 74 | 120010 75 | deb_lookup.sh 76 | WARNING 77 | File has been modified, entry in DEB database differs 78 | no_email_alert 79 | deb_lookup 80 | 81 | 82 | 83 | 5902 84 | UID=\d\d\d, 85 | System account created 86 | 87 | 88 | 89 | command_search.sh 90 | WARNING 91 | Possible suspicious command identified 92 | 93 | 94 | 95 | time_lookup.sh 96 | WARNING 97 | System clock is off 98 | 99 | 100 | 101 | 102 | 120014 103 | ossec-sec 104 | Ignore clock messages from OSSEC server 105 | 106 | 107 | 108 | ldap_lookup 109 | Custom LDAP Lookup Alert 110 | ldap_lookup 111 | 112 | 113 | 114 | 120020 115 | ldap_lookup.sh 116 | OK 117 | Attempted user is an employee 118 | ldap_lookup 119 | 120 | 121 | 122 | 120020 123 | ldap_lookup.sh 124 | WARNING 125 | Attempted user is not an employee 126 | ldap_lookup 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /rules/organization_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.1.1.|1.2.2. 4 | team1|team2 5 | $SECOPS_USERS|blah1|blah2 6 | blah3|blah4jh 7 | nagios|nrpe 8 | $NAGIOS_USERS|git|qualys|cabackup|zimbra|postgres|gitwriter 9 | owencampbell@lawyer.com|DAS@sydenham.lewisham.sch.uk|upgradeteam@admin.in.th|techservice@discuz.org|manager.verificationdepartment@googlemail.com|webaccount@zbavitu.net|info@adimin.com|systemadmniteam@gmail.com|account-supporteam-verification@live.com|tech.dpart@info.lt|hlpdesk7@yahoo.co.uk|webservices@gala.net|admin@coyspu.com.ar|notice@mail2webmaster.com|merijnkatrien@zonnet.nl|notice@mail2webmaster.com|update-check@live.nl|helpweb13@yahoo.com.hk|accountverification@8u8.com|desk.loginupdate@yahoo.com.hk|email.upgrades@gmail.com|helpdeskict1@yahoo.com.hk|edu-accountupdate2008@live.com|webmailteam001@gmail.com|customerhelp_desk@email.com|upgrade.suppteam2008@sanook.com|supp.team2008@ymail.com|s.eamilupgrade@ymail.com|customer.careservice@live.com|abuse-itservice@live.com|upgreatunit@yahoo.com.hk|ldorow@utica.edu|suppteam@mail2webmaster.com|computer.desk@live.com|tech.service18@live.com|customercareunite111@mail2consultant.com|verification_01@live.com 10 | 11 | 12 | 13 | 14 | 15 | 16 | 5402,5403,5501,5502,5715 17 | $NAGIOS_USERS 18 | Ignore nagios/nrpe log 19 | 20 | 21 | 22 | ossec 23 | ossec-sec 24 | Ignore matching on ossec logs that are fed back in 25 | 26 | 27 | 28 | 4721 29 | $SECOPS_NETS 30 | Supress router changes from organization nets 31 | 32 | 33 | 34 | 5400 35 | blah1|blah2|blah3|blah4 36 | sudo use on bastions 37 | 38 | 39 | 40 | 5715 41 | 42 | 43 | Multiple SSH logins from same source IP 44 | authentication_success, 45 | 46 | 47 | 48 | 100020 49 | $SECOPS_NETS 50 | Ignore multiple SSH logins from same source SECOPS networks 51 | authentication_success, 52 | 53 | 54 | 55 | 5710 56 | 57 | Multiple attempt to login using a non-existent user 58 | 59 | 60 | 61 | WARN: Syscheck disabled.|WARN: Rootcheck disabled. 62 | OSSEC service disabled 63 | 64 | 65 | 66 | 100020 67 | garvey 68 | gssapi-with-mic 69 | 130.126.3.77 70 | Ignore multiple SSH logins from garvey by same source IP 71 | 72 | 73 | 74 | authentication_success 75 | lists/ip.list 76 | Successful authentication from bad IP 77 | 78 | 79 | 80 | authentication_success 81 | lists/bhr.list 82 | Successful authentication from previously BHR'd IP 83 | 84 | 85 | 86 | web_scan|recon|sqlinjection|attack 87 | lists/url.list 88 | URL Intel list match 89 | 90 | 91 | 92 | 5712 93 | 5715 94 | 95 | 96 | 97 | lists/networks.list 98 | Success after multiple failed SSH authentications from outside 99 | 100 | 101 | 102 | authentication_success 103 | authentication_failed,authenticaton_failures 104 | 105 | 106 | Success after failed authentication 107 | 108 | 109 | 110 | system_shutdown 111 | 503,504 112 | 113 | Ignore agent started after system shutdown 114 | 115 | 116 | 117 | /contact/|/administrator/|/admin/|/session.php 118 | JDatabaseDriverMysqli 119 | Possible Joomla! exploitation attempt 120 | 121 | 122 | 123 | 5715 124 | root 125 | lists/networks.list 126 | Non-organization root login 127 | 128 | 129 | 130 | 100035 131 | password 132 | Non-organization root login with password 133 | 134 | 135 | 136 | 137 | 15001,15003 138 | lists/ksu.list 139 | Authentication attempted from unvetted ksu user 140 | 141 | 142 | 143 | 144 | 5400 145 | lists/system_users.list 146 | System account executed sudo 147 | 148 | 149 | 150 | 5400 151 | lists/commands.list 152 | Suspicious command executed 153 | 154 | 155 | 156 | $BAD_SMTP 157 | Illegal E-mail-mail list from 2006 by Aashish 158 | 159 | 160 | 161 | OOM for frozen_buffer 162 | Kernel went into write mode 163 | 164 | 165 | 166 | kernel: Program Xnest tried to access /dev/mem|kernel: Program Xnest tried 167 | Possible Phalanx exploit 168 | 169 | 170 | 171 | 5749 172 | Disconnecting: Bad packet length 173 | Possible Phalanx exploit 174 | 175 | 176 | 177 | 178 | authentication_failed|authenticaton_failures|authentication_success|add_user|account_changed|invalid_login 179 | lists/watched_users.list 180 | Watched user alert triggered 181 | 182 | 183 | 184 | 185 | 5400 186 | syslog,sudo 187 | lists/sudo.list 188 | Authentication attempted from unvetted sudo user 189 | 190 | 191 | 192 | authentication_failed|authenticaton_failures|authentication_success|connection_attempt|virus|attacks|invalid_login|syslog 193 | lists/watched_ips.list 194 | Activity from watched IP list detected 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /rules/organization_syscheck_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 550 9 | /var/ossec/lists/ 10 | a CDB list has been modified 11 | no_log 12 | 13 | 14 | 15 | 550 16 | /etc/krb5.keytab 17 | Priority file alert, Kerberos keytab file modified 18 | 19 | 20 | 21 | 550 22 | /etc/khupd2.p2 23 | Phalanx rootkitt 24 | 25 | 26 | 27 | syscheck 28 | /var/ossec/etc/shared/agent.conf 29 | agent.conf was modified 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /rules/overwrite_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | core_dumped|error|attack|bad |illegal |fatal|Segmentation Fault|Corrupted 4 | 5 | 6 | 7 | 8 | 9 | 10 | 500 11 | Ossec started 12 | Ossec server started. 13 | 14 | 15 | 16 | ossec 17 | syscheck_new_entry 18 | File added to the system. 19 | syscheck, 20 | 21 | 22 | 23 | 500 24 | ^ossec: File rotated 25 | Log file rotated. 26 | 27 | 28 | 29 | $NEW_BAD_WORDS 30 | alert_by_email 31 | Unknown problem somewhere in the system. 32 | 33 | 34 | 35 | Non standard syslog message (size too large). 36 | 37 | 38 | 39 | 5702 40 | Possible breakin attempt 41 | (high number of reverse lookup errors). 42 | no_email_alert 43 | 44 | 45 | 46 | 5700 47 | illegal user|invalid user 48 | Attempt to login using a non-existent user 49 | invalid_login,authentication_failed, 50 | no_log 51 | 52 | 53 | 54 | 5700 55 | Connection refused$ 56 | ssh connection refused 57 | no_log 58 | 59 | 60 | 61 | 5100 62 | I/O error: dev |end_request: I/O error, dev 63 | Kernel Input/Output error 64 | 65 | 66 | 67 | authentication_success 68 | 69 | authentication_success 70 | First time user logged in. 71 | 72 | 73 | 74 | 5100 75 | Promiscuous mode enabled| 76 | device \S+ entered promiscuous mode 77 | Interface entered in promiscuous(sniffing) mode. 78 | promisc, 79 | 80 | 81 | 82 | adduser 83 | attacks 84 | 85 | Attacks followed by the addition 86 | of an user. 87 | 88 | 89 | 90 | --------------------------------------------------------------------------------