Browse Source

add concierge-secaudit

guillaume 7 years ago
parent
commit
4cc30f5645
2 changed files with 237 additions and 1 deletions
  1. 13 1
      README.md
  2. 224 0
      src/concierge-secaudit

+ 13 - 1
README.md

@@ -10,7 +10,7 @@ Notify upon issues. Keep noise to a minimum. Keep configuration to a minimum.
 
 
 ### concierge-backup
 ### concierge-backup
 
 
-Create local and remote backups for file and PostgreSQL database backups. 
+Create local and remote backups of directories and PostgreSQL databases. 
 
 
 Configuration: /etc/concierge/backup.cfg
 Configuration: /etc/concierge/backup.cfg
 
 
@@ -22,6 +22,18 @@ Validate system configuration.
 
 
 Configuration: none
 Configuration: none
 
 
+### concierge-secaudit
+
+Audit filesystem permissions for possible security issues: 
+* World-readable private keys (ssh, Let's Encrypt) and passwords (Git, SVN, Sympa, Dolibarr, ...)O
+* World-writable configuration files and scripts (/etc/init.d/*, /etc/profile, ...)
+* World-writable executable search path ($PATH), python search path, and perl include path
+
+This tool only does file permissions checks, and does it imperfectly. 
+You should not rely on this single tool for security auditing. 
+
+Configuration: none
+
 ### concierge-status
 ### concierge-status
 
 
 Check system status.
 Check system status.

+ 224 - 0
src/concierge-secaudit

@@ -0,0 +1,224 @@
+#!/usr/bin/python3
+
+import os
+import pwd
+import subprocess
+import sys
+import glob
+from pathlib import Path
+
+def get_perl_inc():
+  # perl -e "print join $/, values @INC"
+  try:
+    res = subprocess.check_output(['perl', '-e', 'print join $/, values @INC'])
+    return res.decode('utf-8').split("\n")
+  except FileNotFoundError:
+    return []
+
+readPatterns = [
+  '/etc/ssl/*_key',
+  '~/.ssh/identity',
+  '~/.ssh/id_dsa',
+  '~/.ssh/id_rsa',
+  '~/.ssh/id_ecdsa',
+  '~/.ssh/id_ed25519',
+  '~/.git-credentials',
+  '~/.config/git/credentials',
+  '~/.subversion/auth',
+  '%APPDATA%/Subversion/auth/',
+  '~/.hgrc',
+  '~/.netrc',
+  '~/.config/filezilla/filezilla.xml',
+  '~/.xchat2/servlist_.conf',
+  '~/.mozilla/firefox/*/key3.db',
+  '~/.mozilla/firefox/*/logins.json',
+  '~/.icedove/*/key3.db',
+  '~/.icedove/*/logins.json',
+  '~/.thunderbird/*/key3.db',
+  '~/.thunderbird/*/logins.json',
+  '~/.purple/accounts.xml',
+  '~/.python_history',
+  '~/.bash_history',
+  '~/.config/sonata/sonatarc',
+  '/etc/sympa/sympa.conf*',
+  '/etc/dolibarr/conf.php*',
+  '/etc/letsencrypt/archive/*/privkey*.pem',
+  '/etc/letsencrypt/accounts/*/directory/*/private_key.json',
+  '/etc/letsencrypt/keys/*.pem',
+  '/etc/cups/ssl/*.key',
+  '/etc/unbound/*.key'
+  ]
+
+writePatterns = [
+  '/etc/ld.so.conf',
+  '/etc/ld.so.conf.d/*',
+  '/etc/init.d/',
+  '/etc/rc.local',
+  '/etc/rd*.d/*',
+  '/etc/cron.d/*',
+  '/etc/cron.hourly/*',
+  '/etc/cron.weekly/*',
+  '/etc/cron.daily/*',
+  '/etc/cron.monthly/*',
+  '~/.aliases',
+  '/etc/update-motd.d/*',
+  '/etc/profile', # dash shell, bash shell
+  '/etc/profile.d/*.sh', # dash shell, bash shell
+  '~/.profile',
+  '~/.inputrc',
+  '/etc/bash.bashrc', # bash shell
+  '~/.bashrc', # bash shell
+  '~/.bash_aliases', # bash shell
+  '/etc/bash_completion', # bash shell
+  '/etc/bash_completion.d/*', # bash shell
+  '~/.bash_completion', # bash shell
+  '~/.bash_profile', # bash shell
+  '~/.bash_login', # bash shell
+  '~/.bash_logout', # bash shell
+  '~/.config/fish/config.fish', # fish shell
+  '~/.zshrc', # zsh shell
+  '/etc/csh.cshrc', # tcsh shell
+  '/etc/csh.login', # tcsh shell
+  '~/.tcshrc', # tcsh shell
+  '~/.cshrc', # tcsh shell
+  '~/.login', # tcsh shell
+  '/etc/csh.logout', # tcsh shell
+  '~/.logout' # tcsh shell
+  ]
+
+# Default values for Debian
+# TODO read values from /etc/login.defs
+UID_MIN=1000
+UID_MAX=60000
+
+homePaths = []
+for pwd in pwd.getpwall():
+  if pwd.pw_uid in range(UID_MIN, UID_MAX):
+    homePaths.append(pwd.pw_dir)
+
+def patternWalk(pattern):
+  if pattern == '~' or pattern[:2] == '~/':
+    for homePath in homePaths:
+      yield from patternWalk(homePath + pattern[1:])
+  else:
+    yield from glob.glob(pattern)
+
+writePatternsParents = [
+  ]
+
+for writePattern in writePatterns:
+  path = Path(writePattern).parent
+  homePath = Path('~')
+  while path not in writePatternsParents:
+    writePatternsParents.append(path)
+    # Do not walk up past the home path for now
+    if path != homePath:
+      path = path.parent
+  # Now, walk past the home path(s)
+  for homePath in patternWalk('~'):
+    path = Path(homePath).parent
+    while path not in writePatternsParents:
+      writePatternsParents.append(path)
+      path = path.parent
+
+def hasMode(path, mode):
+  try:
+    return ( path.stat().st_mode & mode ) == mode
+  except PermissionError:
+    return ( 0o0000 & mode ) == mode
+
+def isWorldReadable(path):
+  if hasMode(path, 0o0004):
+    anchor = Path(path.anchor)
+    path = path.parent
+    while path != anchor:
+      if hasMode(path, 0o0001):
+        path = path.parent
+      else:
+        return False
+    return True
+  else:
+    return False
+
+def isWorldWritable(path):
+  anchor = Path(path.anchor)
+  # If the path do not exist, find a parent that does
+  while path != anchor and not path.exists():
+    path = path.parent
+  if hasMode(path, 0o0002):
+    path = path.parent
+    while path != anchor:
+      if hasMode(path, 0o0001):
+        path = path.parent
+      else:
+        return False
+    return True
+  else:
+    return False
+
+exceptions = ''
+
+def logExceptions(message, pathList):
+  global exceptions
+  if len(pathList) > 0:
+    exceptions += "%s: \n" % message
+    for path in (pathList):
+      exceptions += " * %s\n" % path.as_posix()
+    exceptions += "\n"
+
+def printExceptions():
+  global exceptions
+  print(exceptions, end='')
+
+readExceptions = []
+for pattern in readPatterns:
+  for strPath in patternWalk(pattern):
+    path = Path(strPath)
+    if isWorldReadable(path):
+      readExceptions.append(path)
+
+logExceptions('These paths are world readable', readExceptions)
+
+writeExceptions = []
+for pattern in writePatterns:
+  for strPath in patternWalk(pattern):
+    path = Path(strPath)
+    if isWorldWritable(path):
+      writeExceptions.append(path)
+
+logExceptions('These paths are world-writable', writeExceptions)
+
+parentWriteExceptions = []
+for pattern in writePatternsParents:
+  for strPath in patternWalk(str(pattern)):
+    path = Path(strPath)
+    if isWorldWritable(path):
+      parentWriteExceptions.append(path)
+
+logExceptions('These paths are world-writable', parentWriteExceptions)
+
+execpathWriteExceptions = []
+for strPath in os.get_exec_path():
+  path = Path(strPath)
+  if isWorldWritable(path):
+    execpathWriteExceptions.append(path)
+
+logExceptions('These executable search paths are world-writable', execpathWriteExceptions)
+
+pythonpathWriteExceptions = []
+for strPath in sys.path:
+  path = Path(strPath)
+  if isWorldWritable(path):
+    pythonpathWriteExceptions.append(path)
+
+logExceptions('These python search paths are world-writable', pythonpathWriteExceptions)
+
+perlpathWriteExceptions = []
+for strPath in get_perl_inc():
+  path = Path(strPath)
+  if isWorldWritable(path):
+    perlpathWriteExceptions.append(path)
+
+logExceptions('These perl include paths are world-writable', perlpathWriteExceptions)
+
+printExceptions()