Permalink
Browse files

Merge pull request #7 from ygjb/master

Please merge the changes from my version of Garmr to the original project.
  • Loading branch information...
2 parents 8c6615b + 68371f6 commit 082f2749ec599a04dfc00ccea1265ec14aff4c09 @AutomatedTester committed Sep 26, 2011
Showing with 666 additions and 429 deletions.
  1. +6 −0 .gitignore
  2. +22 −17 README.md
  3. +0 −1 __init__.py
  4. +29 −0 config.txt
  5. +125 −0 corechecks.py
  6. +25 −0 djangochecks.py
  7. +89 −217 garmr.py
  8. +116 −0 reporter.py
  9. +254 −0 scanner.py
  10. +0 −45 setup.py
  11. +0 −149 test/test_reports.py
View
@@ -3,3 +3,9 @@ dist/
*.egg-info/
*.pyc
garmr-results.xml
+
+.project
+
+.pydevproject
+
+targets.txt
View
@@ -1,28 +1,33 @@
# Garmr
-Garmr is a checking that a site meets the basic requirements from a security point of view.
-It checks what the correct HTTP calls are allowed and others are blocked. It is installable from PyPi.
+Garmr is a tool to inspect the responses from websites for basic security requirements.
+
+Garmr includes a set of core test cases implemented in corechecks that are derived from
+the Secure Coding Guidelines that can be found at [https://wiki.mozilla.org/WebAppSec/Secure_Coding_Guidelines]
## Installation
-To install it is a simple case of
- sudo pip install garmr
+This version of Garmr :
+* does not support pip. Grab the source from git
+* requires Requests > 0.6.2-dev, which can be installed by following the instructions here:
+** http://docs.python-requests.org/en/latest/user/install/#get-the-code
## Usage
-garmr -u http://application.under.test/path
-
-This will create a file called garmr-results.xml which will have the results of the
-tests stored in it.
-
-### Options
-
-* "-u", "--url": Url to be tested
-* "-f", "--file": File name with URLS to test, Currently not available
-* "-x", "--xunit": Name of file that you wish to write to. Defaults to garmr-results.xml
+usage: garmr.py [-h] [-u TARGETS] [-m MODULES] [-f TARGET_FILES] [-p] [-d]
+optional arguments:
+ -h, --help show this help message and exit
+ -u TARGETS, --url TARGETS
+ add a target to test
+ -m MODULES, --module MODULES
+ load a test suite
+ -f TARGET_FILES, --file TARGET_FILES
+ File with urls to test
+ -p, --force-passive Force passives to be run for each active test
+ -d, --dns Skip DNS resolution when registering a target.
## Tasks
-
-If you want to see what is currently being worked on you can see it on the
-[Pivotal Tracker](https://www.pivotaltracker.com/projects/285905)
+* Implement sequences (i.e. a series of ActiveTests that once invoked, maintains a cookie jar until the list of URLs is exhausted)
+* Implement a proper detailed reporter; currently a range of data is accumulated, but never reported.
+* Implement more checks
View
@@ -1 +0,0 @@
-
View
@@ -0,0 +1,29 @@
+[Garmr]
+force-passives = False
+module = corechecks, djangochecks
+reporter = reporter.AntXmlReporter
+output = garmr-results.xml
+dns = True
+
+[corechecks.StsUpgradeCheck]
+enabled = True
+
+[djangochecks.AdminAvailable]
+enabled = True
+path = console
+
+[corechecks.RobotsTest]
+enabled = True
+
+[corechecks.StsHeaderPresent]
+enabled = True
+
+[corechecks.SecureAttributePresent]
+enabled = True
+
+[corechecks.HttpOnlyPresent]
+enabled = True
+
+[corechecks.XfoPresent]
+enabled = True
+
View
@@ -0,0 +1,125 @@
+from urlparse import urlparse
+import requests
+from scanner import ActiveTest, PassiveTest, Scanner, get_url
+
+
+class HttpOnlyAttributePresent(PassiveTest):
+ description = "Inspect the Set-Cookie: header and determine if the HttpOnly attribute is present."
+ 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("Skip", "No cookie is set by this response.", None)
+ return result
+
+class SecureAttributePresent(PassiveTest):
+ description = "Inspect the Set-Cookie: header and determine if the Secure attribute is present."
+ 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("Skip", "No cookie is set by this response.", None)
+ return result
+
+
+class StrictTransportSecurityPresent(PassiveTest):
+ secure_only = True
+ description = "Check if the Strict-Transport-Security header is present in TLS requests."
+ def analyze(self, response):
+ stsheader = "Strict-Transport-Security"
+ sts = stsheader in response.headers
+ if sts == False:
+ result = self.result("Fail", "Strict-Transport-Security header not found.", None)
+ else:
+ result = self.result("Pass", "Strict-Transport-Security header present.", response.headers[stsheader])
+ return result
+
+class XFrameOptionsPresent(PassiveTest):
+ description = "Check if X-Frame-Options header is present."
+ def analyze(self, response):
+ xfoheader = "X-Frame-Options"
+ xfo = xfoheader in response.headers
+ if xfo == False:
+ result = self.result("Fail", "X-Frame-Options header not found.", None)
+ else:
+ result = self.result("Pass", "X-Frame-Options header present.", response.headers[xfoheader])
+ return result
+
+class RobotsTest(ActiveTest):
+ run_passives = True
+ description = "Check for the presence of a robots.txt file. If save_contents is true, the contents will be saved."
+ config = {"save_contents" : "False"}
+ def do_test(self, url):
+ u = urlparse(url)
+ roboturl="%s://%s/robots.txt" % (u.scheme, u.netloc)
+ response = requests.get(roboturl)
+ if response.status_code == 200:
+ result = self.result("Pass", "A robots.txt file is present on the server",
+ response.content if self.config["save_contents"].lower() == "true" else None)
+ else:
+ result = self.result("Fail", "No robots.txt file was found.", None)
+ return (result, response);
+
+class StsUpgradeCheck(ActiveTest):
+ insecure_only = True
+ run_passives = False
+ description = "Inspect the Strict-Transport-Security redirect process according to http://tools.ietf.org/html/draft-hodges-strict-transport-sec"
+
+ 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 = 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 = stsheader in response2.headers
+ else:
+ bad_redirect = True
+
+ success = invalid_header == False and is_redirect == True and correct_header == True
+ if success == True:
+ message = "The STS upgrade occurs properly (no STS header on HTTP, a 301 redirect, and an STS header in the subsequent request."
+ else:
+ message = "%s%s%s%s" % (
+ "The initial HTTP response included an STS header (RFC violation)." if invalid_header else "",
+ "" if is_redirect else "The initial HTTP response should be a 301 redirect (RFC violation see ).",
+ "" if correct_header else "The followup to the 301 redirect must include the STS header.",
+ "The 301 location must use the https scheme." if bad_redirect else ""
+ )
+ result = self.result("Pass" if success else "Fail", message, None)
+ return (result, response1)
+
+
+def configure(scanner):
+ if isinstance(scanner, Scanner) == False:
+ raise Exception("Cannot configure a non-scanner object!")
+ scanner.register_check(StrictTransportSecurityPresent())
+ scanner.register_check(XFrameOptionsPresent())
+ scanner.register_check(RobotsTest())
+ scanner.register_check(StsUpgradeCheck())
+ scanner.register_check(HttpOnlyAttributePresent())
+ scanner.register_check(SecureAttributePresent())
View
@@ -0,0 +1,25 @@
+from urlparse import urlparse
+import requests
+from scanner import ActiveTest, PassiveTest, Scanner, get_url
+
+
+class AdminAvailable(ActiveTest):
+ run_passives = True
+ config = {"path" : "admin"}
+
+ def do_test(self, url):
+ u = urlparse(url)
+ adminurl="%s://%s/%s" % (u.scheme, u.netloc, self.config["path"])
+ response = requests.get(adminurl)
+ if response.status_code == 200:
+ result = self.result("Pass", "Django admin page is present at %s." % adminurl, response.content)
+ else:
+ result = self.result("Fail", "Default Django admin page is not present at %s" % adminurl, None)
+ return (result, response);
+
+
+def configure(scanner):
+ if isinstance(scanner, Scanner) == False:
+ raise Exception("Cannot configure a non-scanner object!")
+ scanner.register_check(AdminAvailable())
+
Oops, something went wrong.

0 comments on commit 082f274

Please sign in to comment.