Writing Custom OSSEC Rules

Step 1: Add the log files you want to monitor to ossec.conf

Do this step on ossec server and agent both. Open up /var/ossec/etc/ossec.conf and, near the end of the file (before </ossec_config>), add the following:

<localfile>       
  <log_format>syslog</log_format>
  <location>/var/log/my_app_log.log</location>
</localfile>

I used syslog here as it’s recommended for log files that have one entry per line. Available values for log_format are syslog, snort-full, snort-fast, squid, iis, eventlog (for Windows event logs), mysql_log, postgresql_log, nmapg or apache.

If you’re monitoring log files that contain changeable dates, OSSEC understands strftime variables, so, for example, if your log file is /var/log/apache2/access.log.2010-09-25, you can set location to <location>/var/log/apache2/access.log.%Y-%m-%d.

Tip: You can render a strftime variable at the command line to verify it quickly. Just type date +%X at the command line, where X is the stftime variable. date +%Y-%m-%d gives us the string we need for our access logs, date +%s gives us Epoch time UTC.

Step 2: Create a custom decoder

OSSEC uses decoders to parse log files. After it finds the proper decoder for a log, it will parse out fields defined in /etc/decoders.xml, then compare these values to values in rule files – and will trigger an alert when values in the deciphered log file match values specified in rule files. These values can also be passed to active response commands, if you’ve got them enabled.

The log line I want to trigger an alert for looks something like this:

2010-09-25 15:28:42 WARN ForceField IP:127.0.0.1@script_x: forcefield on; enabled forcefield arbitrarily!

Do this step on ossec server. Open up /var/ossec/etc/local_decoder.xml (you can also use decoder.xml, which already exists, but using local_decoder.xml will assure that you don’t overwrite it on upgrade). First, we want to create a decoder that will match the first part of the log entry. We’ll use the date and first few characters to grab it using a regular expression. Note that OSSEC has its own sort of interpretation of regex, so don’t try to get fancy. I spent a lot of time pulling my hair out after using \d{4} type regex syntax – think simpler and you’ll have more success: you have to use \d\d\d\d instead.

In the following decoder, we start at the beginning of the line (\^), then match the digits in YYYY-MM-DD HH:MM:SS. After the date and time, I may have a few different log levels listed, INFO, WARN, DEBUG, etc., so I’ll just match any number of characters greater than 0 (\w+). We also want to end on something relatively unique since the log level regex I used is so loosy-goosy, and I know this is a ForceField alert and all ForceField alerts will contain ForceField, so I’ll use the following.

<decoder name="forcefield">  
  <prematch>^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \w+ ForceField</prematch>
</decoder>

Let’s take a break here, and see if this triggers our alert. Save and exit local_decoder.xml, then run /var/ossec/bin/ossec-logtest.

When it comes up, paste your log line:

2010-09-25 15:28:42 WARN ForceField IP:127.0.0.1@script_x: forcefield on; enabled forcefield

**Phase 1: Completed pre-decoding.       
full event: '2010-09-25 15:28:42 WARN ForceField IP:127.0.0.1@script_x: forcefield on; enabled forcefield arbitrarily!'       
hostname: 'my_system'       
program_name: '(null)'       
log: '2010-09-25 15:28:42 WARN ForceField IP:127.0.0.1@script_x: forcefield on; enabled forcefield arbitrarily!'
**Phase 2: Completed decoding.       
decoder: 'forcefield'

You should see forcefield show up as the decoder. Great! Now, let’s parse out the values we care about.
In case you get an issue like “No decoders match” in ossec-logtest, please check your decoder file. It might be most probably due to some syntax error in your decoder xml.

Re-open local_decoder.xml and, beneath your forcefield decoder, create a new decoder:

<decoder name="forcefield-alert">  
  <parent>forcefield</parent>  
  <regex offset="after_parent">IP:(\d+.\d+.\d+.\d+)@(\w+): (forcefield \w+); (\.*)</regex>  
  <order>srcip,url,action,extra_data</order>
</decoder>

So, what’d we do here?

The obvious stuff first: We gave it a name, and designated forcefield-alert as a child of forcefield. Whenever a log matches the forcefield decoder, it’ll then be decoded using forcefield-alert to extract the data fields to match on.

Now for the fun stuff…First, we set the offset to “after_parent” – this means that OSSEC starts looking for matches after the ‘prematch’ stuff (date, time, & ForceField) we specified inside the parent forcefield.

So our log line actually looks like this:

2010-09-25 15:28:42 WARN ForceField IP:127.0.0.1@script_x: forcefield on; enabled forcefield arbitrarily!`

But after extracting the pre-match data, our log line, in OSSEC’s brain, looks like this:

IP:127.0.0.1@script_x: forcefield on; enabled forcefield arbitrarily!

So what do we care about? What fields do we want to test again? A good rule is to decode any data that you want to match inside a rule as well as any data you might need to initiate an active response. I set these items to bold below:

IP:127.0.0.1@script_x: forcefield on; enabled forcefield arbitrarily!

OSSEC only allows specific field definitions. These can be found at the top of the local_decoder.xml file. For the purposes of our log file, we’ll want the IP, the script, the action taken by the system, and the additional data.

When creating the regex for OSSEC, we extract all data inside parenthesis, so we build our regex like this:

IP:(\d+.\d+.\d+.\d+)@(\w+): (forcefield \w+); (\.*)

Then, to specify which parenthetical regex is which field, you add the <order> line, using available fields in decoders.xml:

<order>srcip,url,action,extra_data</order>

Save your local_decoder.xml and let’s run the log file through ossec-logtest again.

ossec-testrule: Type one log per line.
2010-09-25 15:28:42 WARN ForceField IP:127.0.0.1@script_x: forcefield on; enabled forcefield arbitrarily!
**Phase 1: Completed pre-decoding.       
full event: '2010-09-25 15:28:42 WARN ForceField IP:127.0.0.1@script_x: forcefield on; enabled forcefield arbitrarily!'       
hostname: 'my_system'       
program_name: '(null)'       
log: '2010-09-25 15:28:42 WARN ForceField IP:127.0.0.1@script_x: forcefield on; enabled forcefield arbitrarily!'
**Phase 2: Completed decoding.       
decoder: 'forcefield'       
srcip: '127.0.0.1'       
url: 'script_x'       
action: 'forcefield on'       
extra_data: 'enabled forcefield arbitrarily!'

Looks good! It found our decoder and extracted the fields the way we want ‘em. Now, we’re ready to write local rules.

Step 3: Write custom rules

Do this on ossec server. Open /var/ossec/local_rules.xml and add rules. First, we create a group, and a “catch-all” rule to run against any log that is decoded by our forcefield decoder. We set this as level 0 because we don’t want it to trigger an alert:

<group name="forcefield">
  <rule id="700005" level="0">
    <decoded_as>forcefield</decoded_as>   
    <description>Custom Forcefield Alert</description>
  </rule>
</group>

Next, we add dependent rules that trigger if the action matches what’s specified in the rule. <if_sid> specifies the dependency:

<group name="forcefield">
  <rule id="700005" level="0">    
    <decoded_as>forcefield</decoded_as>
    <description>Custom Forcefield Alert</description>
  </rule>  
  <!-- Alert if forcefield enabled -->  
  <rule id="700006" level="12">    
    <if_sid>700005</if_sid>    
      <action>forcefield on</action>    
      <description>Forcefield enabled!</description>  
  </rule>  
  <!-- Alert if forcefield disabled -->    
  <rule id="700007" level="7">    
    <if_sid>700005</if_sid>    
      <action>forcefield off</action>    
      <description>Forcefield off!</description>  
  </rule>  
  <rule id="700008" level="14">    
    <if_sid>700005</if_sid>    
      <action>forcefield hyperdrive</action>    
      <description>Forcefield in hyperdrive, watch out!</description>  
  </rule></group>

Save your local_rules.xml file, and let’s test it again:

ossec-testrule: Type one log per line.
2010-09-25 15:28:42 WARN ForceField IP:127.0.0.1@script_x: forcefield on; enabled forcefield arbitrarily!
**Phase 1: Completed pre-decoding.       
full event: '2010-09-25 15:28:42 WARN ForceField IP:127.0.0.1@script_x: forcefield on; enabled forcefield arbitrarily!'       
hostname: 'my_system'       
program_name: '(null)'       
log: '2010-09-25 15:28:42 WARN ForceField IP:127.0.0.1@script_x: forcefield on; enabled forcefield arbitrarily!'
**Phase 2: Completed decoding.       
decoder: 'forcefield'       
srcip: '127.0.0.1'       
url: 'script_x'       
action: 'forcefield on'       
extra_data: 'enabled forcefield arbitrarily!'
**Phase 3: Completed filtering (rules).       
Rule id: '700006'       
Level: '12'       
Description: 'Forcefield enabled!'
**Alert to be generated.`

Cool — now we’re ready to restart OSSEC and check alerts. When restarting OSSEC, you may find that the new log file that you’re using should exist before you restart OSSEC — if it doesn’t find it, it ignores it. Also, when writing your own rules, set levels specific to your OSSEC deployment — for example, if you’ve enabled active response and want to trigger it, make sure you extract the srcip using your decoder and set the level in the rule to match the level specific to your active response command in ossec.conf.

You’ll probably find that you need to do some tuning, and that some of the alerts you receive will trigger unwanted alerts if they fall through the decoder sieve. I haven’t figured out a way to exclude the file from inspection if it fails to match any decoder (if you know of one, let me know!), but the solution I’ve used is to create a new local rule that matches based on the syslog sid and match, like so:

    <rule id="100009" level="0">
      <if_sid>1002</if_sid>
      <match>Some string in the log I don't want to see</match>
      <description>Don't syslog alert on this one</description>
    </rule>

Repeat for each false positive. It’d be really useful to only allow a single decoder to work on a log file – if anyone knows how to do that, let me know!

OSSEC custom rules: https://apassionatechie.wordpress.com/2019/12/01/ossec-custom-rules-examples/

OSSEC: Writing your own rules


OSSEC cutom decoders, rules and alerts: https://apassionatechie.wordpress.com/2019/12/01/writing-ossec-custom-rules-decoders-and-alerts/
OSSEC issues: https://apassionatechie.wordpress.com/2019/12/01/warn-waiting-for-server-reply-not-started-ossec-agent/

OSSEC start problem due to keys

How to trust self-signed certificate in cURL command line?

Problem:

I’ve created a self-signed certificate for foo.localhost using a Let’s Encrypt recommendation using this Makefile:

include ../.env

configuration = csr.cnf
certificate = self-signed.crt
key = self-signed.key

.PHONY: all
all: $(certificate)

$(certificate): $(configuration)
    openssl req -x509 -out $@ -keyout $(key) -newkey rsa:2048 -nodes -sha256 -subj '/CN=$(HOSTNAME)' -extensions EXT -config $(configuration)

$(configuration):
    printf "[dn]\nCN=$(HOSTNAME)\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:$(HOSTNAME)\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth" > $@

.PHONY: clean
clean:
    $(RM) $(configuration)

I’ve then assigned that to a web server. I’ve verified that the server returns the relevant certificate:

$ openssl s_client -showcerts -connect foo.localhost:8443 < /dev/null
CONNECTED(00000003)
depth=0 CN = foo.localhost
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = foo.localhost
verify error:num=21:unable to verify the first certificate
verify return:1
---
Certificate chain
 0 s:/CN=foo.localhost
   i:/CN=foo.localhost
-----BEGIN CERTIFICATE-----
[…]
-----END CERTIFICATE-----
---
Server certificate
subject=/CN=foo.localhost
issuer=/CN=foo.localhost
---
No client certificate CA names sent
Peer signing digest: SHA512
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 1330 bytes and written 269 bytes
Verification error: unable to verify the first certificate
---
New, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES128-GCM-SHA256
    Session-ID: […]
    Session-ID-ctx: 
    Master-Key: […]
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket:
    […]

    Start Time: 1529622990
    Timeout   : 7200 (sec)
    Verify return code: 21 (unable to verify the first certificate)
    Extended master secret: no
---
DONE

How do I make cURL trust it without modifying anything in /etc? --cacert does not work, presumably because there is no CA:

$ curl --cacert tls/foo.localhost.crt 'https://foo.localhost:8443/'
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

The goal is to enable HTTPS during development:

  • I can’t have a completely production-like certificate without a lot of work to enable DNS verification in all development environments. Therefore I have to use a self-signed certificate.
  • I still obviously want to make my development environment as similar as possible to production, so I can’t simply ignore any and all certificate issues. curl -k is like catch (Exception e) {}in this case – nothing at all like a browser talking to a web server.

In other words, when running curl [something] https://project.local/api/foo I want to be confident that

  1. if TLS is configured properly except for having a self-signed certificate the command will succeed and
  2. if I have any issues with my TLS configuration except for having a self-signed certificate the command will fail.

Using HTTP or --insecure fails the second criterion.

Solution:

Following these steps should solve your issue:

  1. Download and save the self-signed certificate: echo quit | openssl s_client -showcerts -servername "${API_HOST}" -connect "${API_HOST}":443 > cacert.pem
  2. Tell the curl client about it: curl --cacert cacert.pem --location --silent https://${API_HOST}

Also one could use wget and ignore certificates with: wget --no-check-certificate https://${API_HOST}