Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Commit

Permalink
New features, bugfixes
Browse files Browse the repository at this point in the history
* Module support works
* Added HttpOnly, Secure attribute checks
* Added djangochecks module
* Enabled dns override switch
* Added support for target files
* Added support
  • Loading branch information
Yvan Boily committed Sep 14, 2011
1 parent 447cabe commit 00a906c
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 30 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ dist/
*.egg-info/
*.pyc
garmr-results.xml

.project

.pydevproject

targets.txt
Empty file added README.md
Empty file.
55 changes: 47 additions & 8 deletions corechecks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,55 @@
import requests
from scanner import ActiveTest, PassiveTest, Scanner, get_url


class HttpOnlyPresent(PassiveTest):

def analyze(self, response):
cookieheader = "Set-Cookie"
has_cookie = cookieheader in response.headers
if has_cookie:
if "httponly" in response.headers[cookieheader].lower():
result = self.result("Pass", "HttpOnly is set", response.headers[cookieheader])
else:
result = self.result("Fail", "HttpOnly is not set", response.headers[cookieheader])
else:
result = self.result("Pass", "No cookie is set by this request.", None)
return result

class SecureAttributePresent(PassiveTest):

def analyze(self, response):
url = urlparse(response.url)
cookieheader = "Set-Cookie"
has_cookie = cookieheader in response.headers
if has_cookie:
if "httponly" in response.headers[cookieheader].lower():
if url.scheme == "https":
result = self.result("Pass", "HttpOnly is set", response.headers[cookieheader])
else:
result = self.result("Fail", "HttpOnly should only be set for cookies sent over SSL.", response.headers[cookieheader])
else:
if url.scheme == "https":
result = self.result("Fail", "HttpOnly is not set", response.headers[cookieheader])
else:
result = self.result("Pass", "The secure attribute is not set (expected for HTTP)", response.headers[cookieheader])
else:
result = self.result("Pass", "No cookie is set by this request.", None)
return result


class StsHeaderPresent(PassiveTest):

secure_only = True
stsheader = "Strict-Transport-Security"

def analyze(self, response):

sts = selfstsheader in response.headers
stsheader = "Strict-Transport-Security"
sts = stsheader in response.headers
if sts == False:
result = self.result("Fail", "STS header not found.", None)
else:

result = self.result("Pass", "STS header present.", response.headers[self.stsheader])
result = self.result("Pass", "STS header present.", response.headers[stsheader])
return result

class XfoPresent(PassiveTest):
Expand Down Expand Up @@ -45,24 +81,25 @@ def do_test(self, url):
return (result, response);

class StsUpgradeCheck(ActiveTest):
insecure_only = True
run_passives = False
description = "Inspect the STS redirect process."
stsheader = "Strict-Transport-Security"

def do_test(self, url):
stsheader = "Strict-Transport-Security"
u = urlparse(url)
if u.scheme == "http":
correct_header = False
bad_redirect = False
response1 = get_url(url, False)
invalid_header = self.stsheader in response1.headers
invalid_header = stsheader in response1.headers
is_redirect = response1.status_code == 301
if is_redirect == True:
redirect = response1.headers["location"]
r = urlparse(redirect)
if r.scheme == "https":
response2 = get_url(redirect, False)
correct_header = self.stsheader in response2.headers
correct_header = stsheader in response2.headers
else:
bad_redirect = True

Expand All @@ -86,4 +123,6 @@ def configure(scanner):
scanner.register_test(StsHeaderPresent())
scanner.register_test(XfoPresent())
scanner.register_test(RobotsTest())
scanner.register_test(StsUpgradeCheck())
scanner.register_test(StsUpgradeCheck())
scanner.register_test(HttpOnlyPresent())
scanner.register_test(SecureAttributePresent())
24 changes: 24 additions & 0 deletions djangochecks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from urlparse import urlparse
import requests
from scanner import ActiveTest, PassiveTest, Scanner, get_url


class AdminAvailable(ActiveTest):
run_passives = True

def do_test(self, url):
u = urlparse(url)
adminurl="%s://%s/admin" % (u.scheme, u.netloc)
response = requests.get(adminurl)
if response.status_code == 200:
result = self.result("Pass", "Django admin page is present.", response.content)
else:
result = self.result("Fail", "Default Django admin page is not present ", None)
return (result, response);


def configure(scanner):
if isinstance(scanner, Scanner) == False:
raise Exception("Cannot configure a non-scanner object!")
scanner.register_test(AdminAvailable())

42 changes: 33 additions & 9 deletions garmr.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,53 @@
import argparse
from scanner import ActiveTest, PassiveTest, Scanner
import corechecks
import sys
import traceback

def main():
parser = argparse.ArgumentParser(description='Check urls for compliance with Secure Coding Guidelines')
parser.add_argument("-u", "--url", action="append", dest="targets", help="add a target to test")
parser.add_argument("-m", "--module", action="append", dest="modules", help="load a test suite module")
parser.add_argument("-m", "--module", action="append", dest="modules", help="load a test suite")
parser.add_argument("-f", "--file", action="append", dest="target_files", help="File with urls to test")
parser.add_argument("-p", "--force-passive", action="store_true", default=False, dest="force_passives", help ="Force passives to be run for each active test")
parser.add_argument("-d", "--dns", action="store_false", default=True, dest="resolve_target", help ="Skip DNS resolution when registering a target.")
#todo add option to influence DNS resolution before scanning.

args = parser.parse_args()

print "Garmr v0.02"
scanner = Scanner()

scanner.force_passives = args.force_passives
scanner.resolve_target = args.resolve_target


for target in args.targets:
scanner.register_target(target)
if args.targets != None:
for target in args.targets:
scanner.register_target(target)

if args.target_files != None:
for targets in args.target_files:
try:
f = open(targets, "r")
for target in f:
t = target.strip()
if len(t) > 0:
scanner.register_target(t)
except:
Scanner.logger.error("Unable to process the target list in: %s", targets)

corechecks.configure(scanner)
'''
implement module loading here.
modules are collections of classes that extend ActiveTest and PassiveTest
the module must implement the configure(Scanner) function which will register the exposed functions to the scanner.
'''

if args.modules != None:
for module in args.modules:
try:
__import__(module)
m = sys.modules[module]
m.configure(scanner)
except:
Scanner.logger.fatal("Unable to load the requested module [%s]", module)
quit()

scanner.run_scan()


Expand Down
30 changes: 17 additions & 13 deletions scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ class Scanner():
logger.setLevel(logging.DEBUG)

def __init__(self):
self.resolve_target = True
self.force_passives = False
self._passive_tests_ = []
self._active_tests_ = []
self._targets_ = []
Expand All @@ -77,7 +79,7 @@ def scan_target(self, target):
Scanner.logger.info("\t[Skip] [%s] (reason: secure_only)" % test.__class__)
continue
elif (test.insecure_only and is_ssl):
Scanner.logger.info("\t[Skip] [%s] (reason: insecure_only" % test.__class__)
Scanner.logger.info("\t[Skip] [%s] (reason: insecure_only)" % test.__class__)
continue
start = datetime.now()
o = test.execute(target)
Expand Down Expand Up @@ -107,25 +109,27 @@ def scan_target(self, target):

def run_scan(self):
for target in self._targets_:
# try:
self.scan_target(target)
# except Exception, e:
# print "kaboom? %s" % e
try:
self.scan_target(target)
except:
Scanner.logger.error(traceback.format_exc())


def register_target(self, url):
u = urlparse(url)
valid = u.netloc != "" and u.scheme in self._protos_
reason = "%s%s" % ("[bad netloc]" if u.netloc == "" else "", "" if u.scheme in self._protos_ else "[bad scheme]")
# todo - allow an option to disable dns resolution since address resolution may fail, but a host may
# still be reachable through a proxy

# todo - support ipv6 urls
host = u.netloc.split(':')[0]
try:
socket.getaddrinfo(host, None)
except socket.gaierror:
valid = False
reason = "%s[dns]" % reason

if (self.resolve_target):
try:
socket.getaddrinfo(host, None)
except socket.gaierror:
valid = False
reason = "%s[dns]" % reason
else:
valid = True
if valid:
self._targets_.append(url)
Scanner.logger.debug("[target]: %s" % url)
Expand Down

0 comments on commit 00a906c

Please sign in to comment.