#!/usr/bin/ruby
#
# NAME
#   symbiosis-password-test - Test user-passwords for strength.
#
# SYNOPSIS
#   symbiosis-password-test [options]
#
# OPTIONS
#
#    --hourly   Test only passwords which have been recently modified.
#
#     --weekly  Test all user-passwords (default).
#
#    --prefix   Set the directory prefix. Defaults to /srv.
#
#    --help     Show help information.
#
#    --manual   Read the manual for this script.
#
#    --verbose  Be verbose.
#
# DESCRIPTION
#
# This script is designed to test the strength of all user-created
# passwords upon a machine running Bytemark Symbiosis.
#
# The testing of passwords works for
#
#  * FTP passwords
#  * Mail passwords
#
# Basic usage is as simple as:
#
#   symbiosis-password-test [--verbose]
#
# This script will exit 0 if no passwords were found to be weak, otherwise it
# will exit 1.
#
# BUGS
#
# This script does not test crypted passwords -- it is only a quick check.
#
# AUTHOR
#
# Patrick J. Cherry <patrick@bytemark.co.uk>
#

require 'getoptlong'

help = manual = verbose = false
prefix = "/srv"
#
# Specify the check interval
#
check_all_newer_than = nil

opts = GetoptLong.new(
         [ '--help',       '-h', GetoptLong::NO_ARGUMENT ],
         [ '--manual',     '-m', GetoptLong::NO_ARGUMENT ],
         [ '--verbose',    '-v', GetoptLong::NO_ARGUMENT ],
         [ '--weekly',     '-w', GetoptLong::NO_ARGUMENT ],
         [ '--hourly',     '-o', GetoptLong::NO_ARGUMENT ],
         [ '--prefix',     '-p', GetoptLong::REQUIRED_ARGUMENT ]
)

opts.each do |opt,arg|
  case opt
  when '--help'
    help = true
  when '--manual'
    manual = true
  when '--prefix'
    prefix = arg
  when '--verbose'
    $VERBOSE = true
  when '--weekly'
    check_all_newer_than = nil
  when '--hourly'
    check_all_newer_than = (Time.now - 3600)
  end
end

#
# Output help as required.
#
if help or manual
  require 'symbiosis/utils'
  Symbiosis::Utils.show_help(__FILE__) if help
  Symbiosis::Utils.show_manual(__FILE__) if manual
  exit 0
end

#
# Don't need anything until here.
#
require 'symbiosis/domains'

def verbose(s) ; puts s if $VERBOSE ; end

do_test_ftp        = true
do_test_mailboxes  = true
weak = []

begin
  require 'symbiosis/domain/ftp'
rescue LoadError => err
  verbose "Could not find Symbiosis::Domain ftp methods.  Not testing ftp passwords."
  do_test_ftp = false
end

begin
  require 'symbiosis/domain/mailbox'
rescue LoadError => err
  verbose "Could not find Symbiosis::Domain mail methods.  Not testing mailbox passwords."
  do_test_mailboxes = false
end

require 'cracklib'

Symbiosis::Domains.each(prefix) do |domain|

  verbose "Checking #{domain}"

  #
  # Skip symlinks
  #
  if ( domain.is_alias? )
    verbose "\tSkipping as it is an symlink to #{domain.directory}."
    next
  end


  #
  #  Test FTP single-user passwords
  #
  if do_test_ftp

    ftp_users = []

    if domain.ftp_single_user? and
      (check_all_newer_than.nil? or File.stat(domain.ftp_password_file).mtime > check_all_newer_than)

      ftp_users << domain.ftp_single_user
    else
      verbose "\tNot checking #{domain.ftp_password_file} as it hasn't been modified recently."
    end

    if domain.ftp_multi_user? and
      (check_all_newer_than.nil? or File.stat(domain.ftp_users_file).mtime > check_all_newer_than)

      ftp_users += domain.ftp_multi_users
    else
      verbose "\tNot checking #{domain.ftp_users_file} as it hasn't been modified recently."
    end

    ftp_users.each do |u|
      c = CrackLib::Fascist(u.password)

      if c.ok?
        verbose "\tFTP password for #{u.username} is OK"
      else
        verbose "\tFTP password for #{u.username} is weak -- #{c.reason}"
        if u.username.include?("@")
          weak << "#{domain.ftp_users_file} (#{u.username}): #{c.reason}"
        else
          weak << "#{domain.ftp_password_file} (#{u.username}): #{c.reason}"
        end
      end

    end

  end


  if do_test_mailboxes and domain.mailboxes.length > 0

    verbose "\tChecking mailbox passwords"

    domain.mailboxes.each do |mailbox|
      #
      # Skip mailboxes with no password set.
      #
      if mailbox.password.nil?
        verbose "\tNo password set for #{mailbox.local_part}"
        next
      end

      if check_all_newer_than.nil? or File.stat(mailbox.password_file).mtime > check_all_newer_than

        c = CrackLib::Fascist(mailbox.password)

        if c.ok?
          verbose "\tPassword for #{mailbox.local_part} is OK"
        else
          verbose "\tPassword for #{mailbox.local_part} is weak -- #{c.reason}"
          weak << "#{mailbox.password_file}: #{c.reason}"
        end

      else
        verbose "\tNot checking #{mailbox.password_file} as it hasn't been modified recently."
      end

    end

  end

end

if weak.length > 0
puts <<EOF
Security Alert
--------------

Using weak passwords may allow remote attackers to connect to your
server and read your email.  If your FTP password is weak remote
attackers may be able to connect to your server and download your
private files.

Details
-------

The following file(s) contain weak passwords:

  * #{weak.join("\n  * ")}

Need Help?
----------

For advice on securing your machine please consult the documentation
available upon the symbiosis website:

   http://symbiosis.bytemark.co.uk/

EOF
end

exit (weak.empty? ? 0 : 1)
