# Copyright 2004-2005 SigmaSRC, Inc. All Rights Reserved.
# Apache tightening.

#
# Primary resources & URLs material from:
#
# Apache foundation
#
#	http://apache.org
#
# Book - Hardening Apache, by Tony Mobily
#
# Article on securing apache by Artur Maj:
#
#	http://www.securityfocus.com/infocus/1786
#

# File locations correspond to:
# Red Hat: /etc/httpd, /var/www, /usr/sbin/httpd
# Solaris: /etc/apache, /var/apache, /usr/apache/bin/httpd
# Source install
# Source install (Apache 2.x)

# Where most files are
const apache_files = {
    # Solaris
    "/usr/apache/**",
    "/etc/apache/**",
    "/var/apache/**",
    # Red Hat
    "/usr/lib/httpd/**",
    "/etc/httpd/**",
    "/var/www/**",
    # Source
    "/usr/local/apache/**",
    "/usr/local/apache2/**"
    }

# The httpd binary
const apache_httpd = {
    # Solaris
    "/usr/apache/bin/httpd",
    # Red Hat
    "/usr/sbin/httpd",
    # Source
    "/usr/local/apache/bin/httpd",
    "/usr/local/apache2/bin/httpd"
    }

# Non-documentation files not in 'apache_files'; includes httpd
const apache_outside_files = {
    # Solaris
    "/usr/apache/bin/httpd",
    # XXX Unverified; assume it's all in /usr/apache/*
    # Red Hat files
    "/etc/logrotate.d/httpd",
    "/etc/rc.d/init.d/httpd",
    "/usr/bin/ab",
    "/usr/bin/htdbm",
    "/usr/bin/htdigest",
    "/usr/bin/htpasswd",
    "/usr/bin/logresolve",
    "/usr/lib/httpd",
    "/usr/sbin/apachectl",
    "/usr/sbin/httpd",
    "/usr/sbin/httpd.worker",
    "/usr/sbin/rotatelogs",
    "/usr/sbin/suexec",
    # Red Hat directories
    "/var/cache/mod_proxy",
    # "/var/lib/dav", # This is installed with owner apache; that's OK.
    "/var/log/httpd",
    # Source
    # XXX Haven't investigated what "make install" really installs
    "/usr/local/apache/bin/httpd",
    "/usr/local/apache2/bin/httpd"
    }

# The httpd.conf file
const apache_conf  = {
    # Solaris
    "/etc/apache/httpd.conf",
    # Red Hat
    "/etc/httpd/conf/httpd.conf",
    # Source
    "/usr/local/apache/conf/httpd.conf",
    "/usr/local/apache2/conf/httpd.conf"
    }

# The contents of the root directory of the document tree
const apache_docs  = {
    # Solaris
    "/var/apache/htdocs/*",
    # Red Hat
    "/var/www/html/*",
    # Source
    "/usr/local/apache/htdocs/*",
    "/usr/local/apache2/htdocs/*"
    }

# User & group apache should exist (Maj Article)
proc apache_user_and_group(host: Host):

    let ef1 = host.editfile("/etc/passwd")
    let apache_p = ef1.extract("^apache:", ".*")
    if apache_p == {}:
        host.violation(1,"The apache account should be in /etc/passwd")

    let ef2 = host.editfile("/etc/group")
    let apache_g = ef2.extract("^apache:", ".*")
    if apache_g == {}:
        host.violation(1,"The apache group should be in /etc/group")

#
# if any of these files still exist, then we're probably running on an
# unmodified/unkempt version of apache.
#
proc apache_default_doc_files(host: Host):

    # index.html.* files that should not exist:
    let default = {"index.html.ca", "index.html.cz", "index.html.de",
                   "index.html.dk", "index.html.ee", "index.html.el",
                   "index.html.en", "index.html.es", "index.html.fr",
                   "index.html.it", "index.html.lu", "index.html.nl",
                   "index.html.no", "index.html.pt", "index.html.se"}

    let files = host.glob(apache_docs)

    # need to have something there!
    if files == {}:
       host.leave(0, "Can't find any Apache doc files")

    # hunt for indices... shouldn't be too many files, so do it via brute force
    for file in files:
        for d in default:
            if match(concat(r".*[\\/]", d, "$"), file.filename):
                host.violation(0, "Default index file found (%s), probably a default installation in place", file.filename)


#
# According to apache, suggested permissions are 0755 for just about 
# everything, 0511 for the httpd binary
# 
proc apache_file_perms(host: Host):

    # this does a recursive search, so it might take a long time on
    # web sites that have a lot of files....
    # exclude httpd, it's done by the next rule
    let files = host.glob(apache_files | apache_outside_files)
    
    # need to have something there!
    if files == {}:
       host.leave(0, "Can't find any Apache files")

    host.perms(false, files, host.permission(0755), host.account("root"))

proc apache_httpd_perms(host: Host):

    let files = host.glob(apache_httpd)

    # need to have something there!
    if files == {}:
       host.leave(0, "Can't find the Apache binary")

    host.perms(false, files, host.permission(0511), host.account("root"))

#
# httpd -l lists the modules that are in the Apache binary.  Here is
# the allowed list:
#
#	/home/oracle/product/9.2/Apache/Apache2/bin/httpd -l 
#	Compiled in modules: 
#	core.c 
#	mod_access.c 
#	mod_auth.c 
#	mod_log_config.c 
#	prefork.c 
#	http_core.c 
#	mod_mime.c 
#	mod_dir.c 
#
# Any additional will be flagged (Maj Article)
#
proc apache_modules(host: Host):

    let allowed = {"core.c", "mod_access.c", "mod_auth.c",
                   "mod_log_config.c", "prefork.c", "http_core.c",
                   "mod_mime.c", "mod_dir.c"}

    let files = host.glob(apache_httpd)

    # need to have something there!
    if files == {}:
       host.leave(0, "Can't find the Apache binary")

    for exe in files:
        let eb      = host.editbuffer(host.runscript(exe.filename, "-l"))
        let modules = eb.extract("\.c", "^[^A-z]*(\S+)\s*$")

        let disallowed = modules - allowed
        if disallowed != {}:
            host.violation(0,
                "Only the essential Apache modules are allowed; also found: %s",
                formatset(disallowed))

#
# chroot'd - any way to easily check?
#
# I think that searching for "chroot" (p 190, hardening apache) should work,
# but don't have enough confidence to write a check for this.
#

#
# look through the conf file, ensure vars are set, etc.
#

#
# this just looks for a string match in the conf file, as long as it starts
# a line (ignores comments, etc.)
#
func _apache_conf_grep(host: Host, string: str): bool:

    let files = host.glob(apache_conf)

    # need to have something there!
    if files == {}:
       host.leave(0, "Can't find the Apache configuration files")

    let search = concat("^\s*", string, "\\b.*$")

    for conf in files:
        let ef = host.editfile(conf.filename)
        let found = ef.extract(search, ".*")

        if found != {}:
           return true

    return false

#
# Apache shouldn't be running as root.
#
#
# From the Apache conf file:
#
#	If you wish httpd to run as a different user or group, you must run
#	httpd as root initially and it will switch.
#	[...]
#	User/Group: The name (or #number) of the user/group to run httpd as.
#
proc apache_not_running_as_root(host: Host):

    if (_apache_conf_grep(host, "^\s*User\s+(root|\#0)")):
            host.violation(0, "Apache should not be run as user 'root'")

    if (_apache_conf_grep(host, "^\s*Group\s+(root|sys|\#0|\#3)")):
            host.violation(0, "Apache should not be run as group 'root' or 'sys'")

#
# To disable root's public home page you want this somewhere:
#
#	UserDir disabled root
#
# Hardening Apache, p26
#

proc apache_disable_root_home(host: Host):

    if not (_apache_conf_grep(host, "UserDir\s+disabled\s+root")):
            host.violation(0, "Root's home page should not be exposed")

#
# Don't tell our version number!
#
# Hardening Apache, p27
#

proc apache_minimal_server_tokens(host: Host):

    if not (_apache_conf_grep(host, "ServerTokens\s+Prod")):
            host.violation(0, "Apache should not give out its version number")

#
# function that checks for an explicit section, option, and value; true if
# found, false if not.  Takes two string that correspond to the start and end of a
# section.  E.g. to find if this exists:
#
#	<Directory /> 
#	Options None 
#	</Directory>
#
# You'd say:
#
#	_apache_directive_search(host, "Directory /", "Directory", "Options", "None")):
#
func _apache_directive_search(host: Host, start: str, finish: str, option: str, value: str): bool:

    let search1 = concat(".*(<", start, ">[^<]*</", finish, ">).*$")
    # use the word boundary regexp (\b) to bound this
    let search2 = concat(".*\\b", option, "\\s+", value, "\\b.*")

    let files = host.glob(apache_conf)

    # need to have something there!
    if files == {}:
       host.leave(0, "Can't find the Apache configuration files")

    for conf in files:
        # a hack to do multiline matching
        let eb = host.editbuffer(host.runscript("sed", "/#.*/d", conf.filename, "|", "tr", "\'\012\'", "\'\002\'"))

        let found = eb.extract("", search1)

#       host.violation(0, "FOUND: %s = %s", search1, found)

        # should only be one....
        for line in found:
#           host.violation(0, "SF: %s = %s", search2, line)
            if match(search2, line):
                return true

    return false    

#
#  The following options should be set in the httpd.conf file in
# the "<Directory />" section:
#
#	<Directory /> 
#	Options None 
#	AllowOverride None 
#	Order deny,allow 
#	Deny from all 
#	Options -FollowSymLinks
#	</Directory> 
#
# From Maj Article and apache.org docs
#

proc apache_no_options(host: Host):
    if not (_apache_directive_search(host, "Directory /", "Directory", "Options", "None")):
        host.violation(0, "The Options configuration option must be set to 'None'")

proc apache_no_override(host: Host):
    if not (_apache_directive_search(host, "Directory /", "Directory", "AllowOverride", "None")):
        host.violation(0, "The AllowOverride configuration option must be set to 'None'")

proc apache_correct_order(host: Host):
    if not (_apache_directive_search(host, "Directory /", "Directory", "Order", "deny,allow")):
        host.violation(0, "The Order configuration option must be set to 'deny,allow'")

proc apache_deny_from_all(host: Host):
    if not (_apache_directive_search(host, "Directory /", "Directory", "Deny", "from all")):
        host.violation(0, "The Deny configuration option must be set to 'from all'")

proc apache_do_not_follow_sym(host: Host):
    if not (_apache_directive_search(host, "Directory /", "Directory", "Options", "-FollowSymLinks")):
        host.violation(0, "The Options configuration option must be set to '-FollowSymLinks'")

#
# The following lines prevent .htaccess and .htpasswd files from being 
# viewed by Web clients. 
#
#	<Files ~ "^\.ht">
#	Order allow,deny
#	Deny from all
#	</Files>

proc apache_deny_web_clients(host: Host):

    let files = host.glob(apache_conf)

    # need to have something there!
    if files == {}:
       host.leave(0, "Can't find the Apache configuration files")

    for conf in files:
        # a hack to do multiline matching
        let eb = host.editbuffer(host.runscript("sed", "/#.*/d", conf.filename, "|", "tr", "\'\012\'", "\'\002\'"))

        # one crazy regexp....
        let found = eb.extract("", "^.*(<Files \~ \"\^\\\\.ht\">[^<]+</Files>).*$")
        # should only be one....
        for line in found:
            if (match(".*\\bOrder\s+allow\s*,\s*deny\\b.*Deny\s+from\s+all\\b.*", line) or
                match(".*\\bDeny\s+from\s+all.*\\bOrder\s+allow\s*,\s*deny\\b.*", line)):
                    pass
            else:
                    host.violation(0, "The Deny configuration option must be set to 'from all' and the Order configuration option must be set to 'allow,deny'")

