├── .gitignore ├── LICENSE ├── README.rst └── fim.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Christer Edwards 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of saltstack-fim-module nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | File Integrity Monitoring (FIM) Execution Module 2 | ================================================ 3 | 4 | In a nutshell, this module collects FIM data from a minion. Collected data 5 | includes: 6 | 7 | - hashing algorithm 8 | - file atime 9 | - file checksum 10 | - file ctime 11 | - file gid / group 12 | - file inode 13 | - file mode (permissions) 14 | - file mtime 15 | - file size 16 | - file target (full path) 17 | - file type 18 | - file uid / user 19 | 20 | Runtime 21 | ------- 22 | 23 | This module is flexible regarding what data is captured, and how it is 24 | returned. Primary options (runtime or configured) include: 25 | 26 | - algo (md5, sha1, sha224, sha256 (default), sha384, sha512) 27 | - targets (file or directory path. directories will be recursed) 28 | - filename (output filename to save compressed (gzip) output) 29 | 30 | CLI Example: 31 | 32 | .. code-block:: shell 33 | 34 | salt '*' fim.checksum algo='sha1' targets='['/usr/sbin/sshd', '/etc']' 35 | 36 | salt '*' fim.checksum targets='['/bin', '/sbin', '/usr/bin', '/usr/sbin']' 37 | 38 | salt '*' fim.checksum targets='['/etc']' filename='/var/log/salt/fim.log.gz' 39 | 40 | Example #1: use sha1 hash algorithm to recursively hash the defined targets 41 | list. 42 | 43 | Example #2: use default hash algorithm to recursively hash defined targets 44 | list. 45 | 46 | Example #3: use default hash algorithm to recursively hash defined targets list 47 | and write to defined filename path. 48 | 49 | 50 | Configuration 51 | ------------- 52 | 53 | An example config file, (`/etc/salt/minion.d/fim.conf`) for this module 54 | could look something like this: 55 | 56 | .. code-block:: yaml 57 | 58 | fim: 59 | algo: sha256 60 | filename: /var/log/salt/fim.log.gz 61 | targets: 62 | - /bin 63 | - /sbin 64 | - /usr/bin 65 | - /usr/sbin 66 | - /usr/local/bin 67 | - /usr/local/sbin 68 | 69 | 70 | -------------------------------------------------------------------------------- /fim.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Generic hashing script. Supports files & directories 4 | ''' 5 | from __future__ import absolute_import 6 | 7 | import os 8 | import json 9 | import shutil 10 | import difflib 11 | import logging 12 | from time import strftime 13 | 14 | LOG = logging.getLogger(__name__) 15 | 16 | __virtualname__ = 'fim' 17 | 18 | 19 | def __virtual__(): 20 | if ('file.get_hash' and 'file.stats') in __salt__: 21 | return __virtualname__ 22 | 23 | 24 | def _hasher(algo, target): 25 | ''' 26 | Convenience function to handle hashing 27 | ''' 28 | return __salt__['file.get_hash'](target, algo) 29 | 30 | 31 | def _stats(target): 32 | ''' 33 | Convenience function to handle stats 34 | ''' 35 | return __salt__['file.stats'](target) 36 | 37 | 38 | def checksum(algo='sha256', targets=[], filename='', *args, **kwargs): 39 | ''' 40 | Generate dictionary of hashes and corresponding filenames. 41 | 42 | Supports file paths and or directories. 43 | ''' 44 | checksums = {} 45 | hostname = __salt__['grains.get']('fqdn') 46 | 47 | ## check for preconfigured algos 48 | if not algo: 49 | try: 50 | if __salt__['config.get']('fim:algo'): 51 | algo = __salt__['config.get']('fim:algo') 52 | except KeyError: 53 | LOG.debug('No algorithm defined. Defaulting to sha256') 54 | 55 | ## check for preconfigured targets 56 | if not targets: 57 | try: 58 | if __salt__['config.get']('fim:targets'): 59 | targets = __salt__['config.get']('fim:targets') 60 | except: 61 | return 'No targets defined. Exiting' 62 | 63 | ## iterate through list of targets and generate checksums 64 | for target in targets: 65 | if os.path.isdir(target): 66 | for root, dirs, files in os.walk(target): 67 | for file_ in files: 68 | target = os.path.join(root, file_) 69 | if os.path.isfile(target): 70 | checksums[target] = {'stats': {}} 71 | checksums[target]['stats'] = _stats(target) 72 | checksums[target]['stats'].update({'checksum': _hasher(algo, target)}) 73 | checksums[target]['stats'].update({'hostname': hostname}) 74 | elif os.path.isfile(target): 75 | checksums[target] = {'stats': {}} 76 | checksums[target]['stats'] = _stats(target) 77 | checksums[target]['stats'].update({'checksum': _hasher(algo, target)}) 78 | checksums[target]['stats'].update({'hostname': hostname}) 79 | 80 | return checksums 81 | 82 | 83 | def diff(): 84 | ''' 85 | Generate unified diff of two most recent fim.dat files 86 | ''' 87 | diff = [] 88 | 89 | timestamp = strftime("%Y-%m-%d") 90 | 91 | new_path = __salt__['config.get']('fim:new_path') 92 | old_path = __salt__['config.get']('fim:old_path') 93 | 94 | root_dir = '/var/cache/salt/master/minions/' 95 | 96 | for minion in os.listdir(root_dir): 97 | try: 98 | for line in difflib.unified_diff(open(root_dir + minion + '/' + 'files' + old_path).readlines(), 99 | open(root_dir + minion + '/' + 'files' + new_path).readlines(), n=0): 100 | 101 | prefix_line = False 102 | for prefix in ('---', '+++', '@@'): 103 | if line.startswith(prefix): 104 | prefix_line = True 105 | if not prefix_line: 106 | line = line.strip() 107 | char = line[0] 108 | line = line[1:] 109 | line = line.replace("'", "\"") 110 | line = json.loads(line) 111 | line['diff'] = char 112 | line['timestamp'] = timestamp 113 | diff.append(line) 114 | except IOError: 115 | LOG.error('No previous run to compare. Run fim.rotate.') 116 | 117 | ret = '\n'.join([json.dumps(s) for s in diff]) 118 | return ret 119 | 120 | 121 | def rotate(): 122 | ''' 123 | Rotate the fim data files to .old 124 | ''' 125 | new_path = __salt__['config.get']('fim:new_path') 126 | old_path = __salt__['config.get']('fim:old_path') 127 | 128 | root_dir = '/var/cache/salt/master/minions/' 129 | 130 | for minion in os.listdir(root_dir): 131 | shutil.copy(root_dir + minion + '/' + 'files' + new_path, 132 | root_dir + minion + '/' + 'files' + old_path) 133 | 134 | return 'FIM data rotated.' 135 | 136 | --------------------------------------------------------------------------------