-
Notifications
You must be signed in to change notification settings - Fork 687
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ossec: send journalist about the number of submissions in the past 24h
A cron job runs daily on the app server and updates the /var/lib/securedrop/submissions_today.txt file which contains the number of submissions sent in the past 24h, as created by the manage.py how_many_submissions_today command. The OSSEC agence on the app server runs a command daily, displaying the content of /var/lib/securedrop/submissions_today.txt. The output of the command is sent to the OSSEC server. A new rule is defined on the OSSEC server to send a mail to journalist@localhost when the output is received from the OSSEC agent running on the app server. A new procmail rule is definied on the OSSEC server to encrypt mails received by journalist@localhost and send them to the email defined by the journalist_alert_email ansible variable. A new set of ansible (optional) variables, similar to ossec_alert_gpg_public_key, ossec_gpg_fpr, ossec_alert_email are defined: journalist_alert_gpg_public_key, journalist_gpg_fpr, journalist_alert_email. They are used to upload a journalist public key to the OSSEC server and inserted into the send_encrypted_alarm.sh script which handles mails received by procmail. The modified send_encrypted_alarm.sh script takes one argument (journalist or ossec) and dispatching the mail read from stdin to the corresponding recipient. Integration tests are implemented to verify the following: * manage.py how_many_submissions_today * the app OSSEC agent sends a mail to the journalist address * cover all branches of send_encrypted_alarm.sh
- Loading branch information
Loic Dachary
committed
Jan 27, 2018
1 parent
5b26e69
commit f6f5a50
Showing
12 changed files
with
290 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletions
4
install_files/ansible-base/roles/build-securedrop-app-code-deb-pkg/files/cron.daily
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
#!/bin/bash | ||
|
||
cd /var/www/securedrop || exit 1 | ||
./manage.py how-many-submissions-today |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
root: ossec | ||
journalist: ossec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
import pytest | ||
import testinfra | ||
import time | ||
|
||
|
||
class TestBase(object): | ||
|
||
@pytest.fixture(autouse=True) | ||
def only_mon_staging_sudo(self, host): | ||
if host.backend.host != 'mon-staging': | ||
pytest.skip() | ||
|
||
with host.sudo(): | ||
yield | ||
|
||
def ansible(self, host, module, parameters): | ||
r = host.ansible(module, parameters, check=False) | ||
assert 'exception' not in r | ||
|
||
def run(self, host, cmd): | ||
print(host.backend.host + " running: " + cmd) | ||
r = host.run(cmd) | ||
print(r.stdout) | ||
print(r.stderr) | ||
return r.rc == 0 | ||
|
||
def wait_for(self, fun): | ||
success = False | ||
for d in (1, 2, 4, 8, 16, 32, 64): | ||
if fun(): | ||
success = True | ||
break | ||
time.sleep(d) | ||
return success | ||
|
||
def wait_for_command(self, host, cmd): | ||
return self.wait_for(lambda: self.run(host, cmd)) | ||
|
||
# | ||
# implementation note: we do not use host.ansible("service", ... | ||
# because it only works for services in /etc/init and not those | ||
# legacy only found in /etc/init.d such as postfix | ||
# | ||
def service_started(self, host, name): | ||
assert self.run(host, "service {name} start".format(name=name)) | ||
assert self.wait_for_command( | ||
host, | ||
"service {name} status | grep -q 'is running'".format(name=name)) | ||
|
||
def service_restarted(self, host, name): | ||
assert self.run(host, "service {name} restart".format(name=name)) | ||
assert self.wait_for_command( | ||
host, | ||
"service {name} status | grep -q 'is running'".format(name=name)) | ||
|
||
def service_stopped(self, host, name): | ||
assert self.run(host, "service {name} stop".format(name=name)) | ||
assert self.wait_for_command( | ||
host, | ||
"service {name} status | grep -q 'not running'".format(name=name)) | ||
|
||
|
||
class TestJournalistMail(TestBase): | ||
|
||
def test_procmail(self, host): | ||
self.service_started(host, "postfix") | ||
for (origin, destination) in ( | ||
('journalist', 'journalist'), | ||
('root', 'ossec')): | ||
assert self.run(host, "postsuper -d ALL") | ||
assert self.run( | ||
host, | ||
"echo DEF | mail -s 'abc' {origin}@localhost".format( | ||
origin=origin)) | ||
assert self.wait_for_command( | ||
host, | ||
"mailq | grep -q {destination}@ossec.test".format( | ||
destination=destination)) | ||
self.service_stopped(host, "postfix") | ||
|
||
def test_send_encrypted_alert(self, host): | ||
self.service_started(host, "postfix") | ||
src = "install_files/ansible-base/roles/ossec/files/test_admin_key.sec" | ||
self.ansible(host, "copy", | ||
"dest=/tmp/test_admin_key.sec src={src}".format(src=src)) | ||
|
||
self.run(host, "gpg --homedir /var/ossec/.gnupg" | ||
" --import /tmp/test_admin_key.sec") | ||
|
||
def trigger(who): | ||
assert self.run( | ||
host, "! mailq | grep -q {who}@ossec.test".format(who=who)) | ||
assert self.run( | ||
host, | ||
""" | ||
( echo 'Subject: TEST' ; echo ; echo MYGREATPAYLOAD ) | \ | ||
/var/ossec/send_encrypted_alarm.sh {who} | ||
""".format(who=who)) | ||
assert self.wait_for_command( | ||
host, "mailq | grep -q {who}@ossec.test".format(who=who)) | ||
|
||
# | ||
# encrypted mail to journalist or ossec contact | ||
# | ||
for who in ('journalist', 'ossec'): | ||
assert self.run(host, "postsuper -d ALL") | ||
trigger(who) | ||
assert self.run( | ||
host, | ||
""" | ||
job=$(mailq | sed -n -e '2p' | cut -f1 -d ' ') | ||
postcat -q $job | tee /dev/stderr | \ | ||
gpg --homedir /var/ossec/.gnupg --decrypt 2>&1 | \ | ||
grep -q MYGREATPAYLOAD | ||
""") | ||
# | ||
# failure to encrypt must trigger an emergency mail to ossec contact | ||
# | ||
try: | ||
assert self.run(host, "postsuper -d ALL") | ||
assert self.run(host, "mv /usr/bin/gpg /usr/bin/gpg.save") | ||
trigger(who) | ||
assert self.run( | ||
host, | ||
""" | ||
job=$(mailq | sed -n -e '2p' | cut -f1 -d ' ') | ||
postcat -q $job | grep -q 'Failed to encrypt OSSEC alert' | ||
""") | ||
finally: | ||
assert self.run(host, "mv /usr/bin/gpg.save /usr/bin/gpg") | ||
self.service_stopped(host, "postfix") | ||
|
||
def test_missing_journalist_alert(self, host): | ||
# | ||
# missing journalist mail does nothing | ||
# | ||
assert self.run( | ||
host, | ||
""" | ||
JOURNALIST_EMAIL= \ | ||
bash -x /var/ossec/send_encrypted_alarm.sh journalist | \ | ||
tee /dev/stderr | \ | ||
grep -q 'no notification sent' | ||
""") | ||
|
||
# https://ossec-docs.readthedocs.io/en/latest/manual/rules-decoders/testing.html | ||
def test_ossec_rule_journalist(self, host): | ||
assert self.run(host, """ | ||
set -ex | ||
l="ossec: output: 'head -1 /var/lib/securedrop/submissions_today.txt" | ||
echo "$l" | /var/ossec/bin/ossec-logtest | ||
echo "$l" | /var/ossec/bin/ossec-logtest -U '400600:7:ossec' | ||
""") | ||
|
||
def test_journalist_mail_notification(self, host): | ||
mon = host | ||
app = testinfra.host.Host.get_host( | ||
'ansible://app-staging', | ||
ansible_inventory=host.backend.ansible_inventory) | ||
# | ||
# run ossec & postfix on mon | ||
# | ||
self.service_started(mon, "postfix") | ||
self.service_started(mon, "ossec") | ||
|
||
# | ||
# ensure the submission_today.txt file exists | ||
# | ||
with app.sudo(): | ||
assert self.run(app, """ | ||
cd /var/www/securedrop | ||
./manage.py how-many-submissions-today | ||
test -f /var/lib/securedrop/submissions_today.txt | ||
""") | ||
|
||
# | ||
# empty the mailq on mon in case there were leftovers | ||
# | ||
assert self.run(mon, "postsuper -d ALL") | ||
|
||
# | ||
# start ossec with frequent monitoring of | ||
# submissions_today.txt | ||
# | ||
with app.sudo(): | ||
assert self.run( | ||
app, | ||
"sed -i -e 's/>86400</>7</' /var/ossec/etc/ossec.conf") | ||
self.service_restarted(app, "ossec") | ||
|
||
# | ||
# wait until at least one notification is sent | ||
# | ||
assert self.wait_for_command( | ||
mon, | ||
"mailq | grep -q [email protected]") | ||
|
||
# | ||
# teardown the ossec and postfix on mon and app | ||
# | ||
self.service_stopped(mon, "postfix") | ||
self.service_stopped(mon, "ossec") | ||
|
||
with app.sudo(): | ||
self.service_stopped(app, "ossec") | ||
assert self.run( | ||
app, | ||
"sed -i -e 's/>7</>86400</' /var/ossec/etc/ossec.conf") |
Oops, something went wrong.