├── Garmr ├── __init__.py ├── reporter.py ├── garmr.py ├── corechecks.py └── scanner.py ├── .gitignore ├── webchecks.py ├── setup.py ├── djangochecks.py ├── authchecks.py ├── README.md └── LICENSE /Garmr/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | dist/ 3 | *.egg-info/ 4 | *.pyc 5 | garmr-results.xml 6 | build/ 7 | *.xml 8 | *.txt 9 | .project 10 | 11 | .pydevproject 12 | 13 | targets.txt 14 | -------------------------------------------------------------------------------- /webchecks.py: -------------------------------------------------------------------------------- 1 | from urlparse import urlparse 2 | import requests 3 | from Garmr.scanner import ActiveTest, PassiveTest, Scanner 4 | 5 | 6 | class RobotsTest(ActiveTest): 7 | run_passives = True 8 | description = "Check for the presence of a robots.txt file. If save_contents is true, the contents will be saved." 9 | config = {"save_contents" : "False"} 10 | def do_test(self, url): 11 | u = urlparse(url) 12 | roboturl="%s://%s/robots.txt" % (u.scheme, u.netloc) 13 | sess = self.sessions[self.url] 14 | response = sess.get(roboturl) 15 | if response.status_code == 200: 16 | result = self.result("Pass", "A robots.txt file is present on the server", 17 | response.content if self.config["save_contents"].lower() == "true" else None) 18 | else: 19 | result = self.result("Fail", "No robots.txt file was found.", None) 20 | return (result, response); 21 | 22 | def configure(scanner): 23 | if isinstance(scanner, Scanner) == False: 24 | raise Exception("Cannot configure a non-scanner object!") 25 | scanner.register_check(RobotsTest()) 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | from setuptools import setup 3 | 4 | def read(fname): 5 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 6 | 7 | 8 | def main(): 9 | setup( 10 | name = "Garmr", 11 | version = "0.3", 12 | author = "David Burns, Yvan Boily", 13 | author_email = "dburns at mozilladotcom", 14 | description='A tool for testing a web application for basic security holes', 15 | url = "https://github.com/AutomatedTester/Garmr", 16 | packages=['Garmr'], 17 | long_description=read('README.md'), 18 | entry_points = make_entry_points(), 19 | classifiers=['Development Status :: 3 - Alpha', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)', 22 | 'Operating System :: POSIX', 23 | 'Operating System :: Microsoft :: Windows', 24 | 'Operating System :: MacOS :: MacOS X', 25 | 'Topic :: Software Development :: Testing', 26 | 'Topic :: Software Development :: Libraries', 27 | 'Programming Language :: Python'], 28 | install_requires= ['requests>=0.6.2','BeautifulSoup==3.2.1'] 29 | ) 30 | 31 | 32 | def cmdline_entrypoints(versioninfo, platform, basename): 33 | target = 'Garmr.garmr:main' 34 | if platform.startswith('java'): 35 | points = {'garmr': target} 36 | else: 37 | if basename.startswith("pypy"): 38 | points = {'garmr-%s' % basename: target} 39 | else: # cpython 40 | points = {'garmr-%s.%s' % versioninfo[:2] : target,} 41 | points['garmr'] = target 42 | return points 43 | 44 | def make_entry_points(): 45 | basename = os.path.basename(sys.executable) 46 | points = cmdline_entrypoints(sys.version_info, sys.platform, basename) 47 | keys = list(points.keys()) 48 | keys.sort() 49 | l = ["%s = %s" % (x, points[x]) for x in keys] 50 | return {'console_scripts': l} 51 | 52 | if __name__ == '__main__': 53 | main() 54 | -------------------------------------------------------------------------------- /djangochecks.py: -------------------------------------------------------------------------------- 1 | from urlparse import urlparse 2 | import requests 3 | from Garmr.scanner import ActiveTest, PassiveTest, Scanner, HtmlTest 4 | 5 | 6 | class AdminAvailable(ActiveTest): 7 | run_passives = True 8 | config = {"path" : "admin"} 9 | 10 | def do_test(self, url): 11 | u = urlparse(url) 12 | adminurl="%s://%s/%s" % (u.scheme, u.netloc, self.config["path"]) 13 | sess = self.sessions[self.url] 14 | response = sess.get(adminurl) 15 | if response.status_code == 200: 16 | result = self.result("Pass", "Django admin page is present at %s." % adminurl, response.content) 17 | else: 18 | result = self.result("Fail", "Default Django admin page is not present at %s" % adminurl, None) 19 | return (result, response); 20 | 21 | 22 | class ProvokeError404(ActiveTest): 23 | run_passives = True # we need IsDebugModeReallyEnabled 24 | def do_test(self, url): 25 | sess = self.sessions(url) 26 | url += '76976cd1a3cbadaf77533a' #random garbage 27 | response = sess.get(url) 28 | result = self.result('Skip', 'This test cannot Pass or Fail, because it relies on the subsequent passive IsDebugModeReallyEnabled test', response) 29 | return result, response 30 | 31 | class IsDebugModeReallyEnabled(HtmlTest): 32 | description = '' 33 | secure_only = False 34 | def analyze_html(self, response, soup): 35 | # we dont really analye the soup, but that's ok;p 36 | error_str = "You're seeing this error because you have" #from django source django/views/debug.py - maybe subject to change 37 | if error_str in response.content: 38 | result = self.result('Fail', 'Typical string of echnical 404/500 error page found', None) 39 | else: 40 | result = self.result('Pass', 'Debug strings not found', response) 41 | return result 42 | 43 | 44 | def configure(scanner): 45 | if isinstance(scanner, Scanner) == False: 46 | raise Exception("Cannot configure a non-scanner object!") 47 | raise Exception("Cannot configure a non-scanner object!") 48 | scanner.register_check(AdminAvailable()) 49 | 50 | -------------------------------------------------------------------------------- /authchecks.py: -------------------------------------------------------------------------------- 1 | from urlparse import urlparse 2 | import requests 3 | from Garmr.scanner import ActiveTest, PassiveTest, Scanner 4 | 5 | 6 | class SessionTest(ActiveTest): 7 | pass 8 | 9 | class CaptchaTest(ActiveTest): 10 | pass 11 | 12 | 13 | class LoginTest(ActiveTest): 14 | '''04:43:14 PM) Yvan Boily: so here is an example; this provides a basic configurable authentication check; the username and password fields are configurable, as are the username and password. 15 | the post data is assembled using a built in format that will work with many authentication forms, and the test assumes (naively) that a 200 response is a successful login 16 | (04:43:27 PM) Yvan Boily: (the description needs to be updated :P) 17 | (04:44:22 PM) Yvan Boily: the do_test method on ActiveTest could be simply extended to accept a second and third paramter: 18 | (04:44:43 PM) Yvan Boily: def do_test(self, url, state, preserve): 19 | (04:45:25 PM) Yvan Boily: the state object would be passed in, and the preserve parameter indicates that the test should not modify the state object if it is set to true 20 | (04:45:30 PM) freddy: we could just change it to do_test(self, url, *args) and be more precise in the subclass 21 | (04:46:14 PM) Yvan Boily: it is possible to do that, but I am not a fan of that style. I don't have a better argument than that, so if you want to go that route, feel free :D''' 22 | 23 | run_passives = True 24 | description = "check if login works" 25 | config = { 26 | "uid_field" : "username", 27 | "pwd_field" : "password", 28 | "username" : "admin", 29 | "password" : "admin", 30 | "format" : "%s=%s&%s=%s" 31 | } 32 | 33 | # eventing needs to be implemented 34 | events = { "Pass": SessionTest, 35 | "Error": CaptchaTest, 36 | "Fail": CaptchaTest } 37 | 38 | def do_test(self, url): 39 | u = urlparse(url) 40 | post_data = config['format'] % (config["uid_field"] , config["username"], config["pwd_field"], config["password"]) 41 | response = requests.post(url, post_data) 42 | if "Login successful" in response.content: 43 | # scrape response for indicators of a successful login 44 | result = self.result("Pass", "Authentication was successful", None) 45 | else: 46 | result = self.result("Fail", "Authentication failed", None) 47 | return (result, response) 48 | 49 | 50 | 51 | 52 | 53 | def configure(scanner): 54 | #if isinstance(scanner, Scanner) == False: 55 | # raise Exception("Cannot configure a non-scanner object!") 56 | scanner.register_check(LoginTest()) 57 | 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Garmr 2 | 3 | Garmr is a tool to inspect the responses from websites for basic security requirements. 4 | 5 | Garmr includes a set of core test cases implemented in corechecks that are derived from 6 | the [Mozilla Secure Coding Guidelines](https://wiki.mozilla.org/WebAppSec/Secure_Coding_Guidelines) 7 | 8 | ## Installation 9 | 10 | This version of Garmr requires Requests > 0.8.3 11 | 12 | git clone https://github.com/freddyb/Garmr.git 13 | cd Garmr 14 | sudo python setup.py install 15 | garmr -u http://my.target.app 16 | 17 | ## Usage 18 | usage: Runs a set of tests against the set of provided URLs 19 | [-h] [-u TARGETS] [-f TARGET_FILES] [-S] [-m MODULES] [-D] [-p] [-d] 20 | [-r REPORT] [-o OUTPUT] [-c OPTS] [-e EXCLUSIONS] [--save DUMP_PATH] 21 | 22 | optional arguments: 23 | -h, --help show this help message and exit 24 | -u TARGETS, --url TARGETS 25 | Add a target to test 26 | -f TARGET_FILES, --target-file TARGET_FILES 27 | File with URLs to test 28 | -S, --new-sessions Create new Session for each test 29 | -m MODULES, --module MODULES 30 | Load an extension module 31 | -D, --disable-core Disable corechecks 32 | -p, --force-passive Force passives to be run for each active test 33 | -d, --dns Skip DNS resolution when registering a target 34 | -r REPORT, --report REPORT 35 | Load a reporter e.g. -r reporter.AntXmlReporter 36 | -o OUTPUT, --output OUTPUT 37 | Default output is garmr-results.xml 38 | -c OPTS, --check OPTS 39 | Set a parameter for a check (check:opt=value) 40 | -e EXCLUSIONS, --exclude EXCLUSIONS 41 | Prevent a check from being run/processed 42 | --save DUMP_PATH Write out a configuration file based on parameters 43 | (won't run scan) 44 | 45 | 46 | A TARGET is an http or https scheme url to execute tests against. 47 | e.g. garmr -u http://localhost 48 | 49 | A MODULE is the name of a module; resolving this path needs to be improved 50 | e.g. garmr -m djangochecks (Experimental) 51 | 52 | An OPTS field contains the path and name of the option to set 53 | e.g. garmr -m webchecks -c webchecks.RobotsTest:save_contents=True 54 | 55 | A REPORT is the namespace qualified name of a reporter object or a valid alias (xml is the only current valid alias, and the default) 56 | e.g. garmr -r xml 57 | 58 | An EXCLUSION prevents a check from being executed 59 | e.g. garmr -e WebTouch 60 | 61 | Disable core checks will prevent all of the checks in corechecks from being loaded; this is useful to limit the scope of testing. 62 | 63 | ## Tasks 64 | See [Issues on Github](https://github.com/freddyb/garmr/issues) 65 | 66 | 67 | -------------------------------------------------------------------------------- /Garmr/reporter.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | class Reporter(): 4 | reporters = {} 5 | def start_report(self): 6 | return None 7 | 8 | def start_targets(self): 9 | return None 10 | 11 | def write_target(self, target): 12 | return None 13 | 14 | def start_actives(self): 15 | return None 16 | 17 | def write_active(self, test, result): 18 | return None 19 | 20 | def start_passives(self): 21 | return None 22 | 23 | def write_passive(self, test, result): 24 | return None 25 | 26 | def end_passives(self): 27 | return None 28 | 29 | def end_actives(self): 30 | return None 31 | 32 | def end_targets(self): 33 | return None 34 | 35 | def end_report(self, results): 36 | return "This reporter is unimplemented!" 37 | 38 | class DetailReporter(Reporter): 39 | # TODO Implement detailed reporter 40 | def end_report(self, results): 41 | return "This reporter should emit an XML report that includes all of the the details for each test, including captured data" 42 | 43 | class JsonReporter(Reporter): 44 | def end_report(self, results): 45 | return json.dumps(results, indent = 2, default = str) 46 | 47 | class AntXmlReporter(Reporter): 48 | 49 | def __init__(self): 50 | self.report = "" 51 | self.errtypes = { 'Error' : "error", 'Fail' : "failure", 'Skip' : "skipped"} 52 | 53 | def start_report(self): 54 | self.report = '\n' 55 | 56 | return None 57 | 58 | def start_targets(self): 59 | self.report += "\n" 60 | return None 61 | 62 | def write_target(self, target): 63 | self.states = {} 64 | self.states["Skip"] = 0 65 | self.states["Error"] = 0 66 | self.states["Pass"] = 0 67 | self.states["Fail"] = 0 68 | self.checks = 0 69 | self.current_target = target 70 | self.lines = "" 71 | return None 72 | 73 | def start_actives(self): 74 | return None 75 | 76 | def write_active(self, test, result): 77 | self.states[result["state"]] += 1 78 | self.checks += 1 79 | module, check = ("%s" % test ).split('.')[-2:] 80 | self.lines += '\t\t 0: 56 | scanner.register_target(t) 57 | except: 58 | Scanner.logger.error("Unable to process the target list in: %s", targets) 59 | 60 | # Load built-in modules if required. 61 | if args.disable_core == False: 62 | corechecks.configure(scanner) 63 | 64 | # Configure modules. 65 | # TODO: change the module loading to scan the list of classes in a module and automagically 66 | # detect any tests defined. 67 | if args.modules != None: 68 | for module in args.modules: 69 | try: 70 | __import__(module) 71 | m = sys.modules[module] 72 | m.configure(scanner) 73 | except Exception, e: 74 | Scanner.logger.fatal("Unable to load the requested module [%s]: %s", module, e) 75 | quit() 76 | 77 | # Set up the reporter (allow it to load from modules that are configured) 78 | try: 79 | reporter = args.report.split('.') 80 | if len(reporter) == 1: 81 | scanner.reporter = Reporter.reporters[reporter[0]] 82 | else: 83 | scanner.reporter = getattr(sys.modules[reporter[0]], reporter[1])() 84 | Scanner.logger.info("Writing report to [%s] using [%s]" % (args.output, args.report)) 85 | if isinstance(scanner.reporter, Reporter) == False: 86 | raise Exception("Cannot configure a non-scanner object!") 87 | except Exception, e: 88 | Scanner.logger.fatal("Unable to use the reporter class [%s]: %s", args.report, e) 89 | quit() 90 | 91 | # Disable excluded checks. 92 | if args.exclusions != None: 93 | for exclude in args.exclusions: 94 | scanner.disable_check(exclude) 95 | 96 | # Configure checks 97 | if args.opts != None: 98 | for opt in args.opts: 99 | try: 100 | check = opt.split(":")[0] 101 | key, value = opt[len(check)+1:].split("=") 102 | scanner.configure_check(check, key, value) 103 | except Exception, e: 104 | Scanner.logger.fatal("Invalid check option: %s (%s)", opt, e) 105 | 106 | if args.dump_path != None: 107 | scanner.save_configuration(args.dump_path) 108 | return 109 | 110 | scanner.run_scan() 111 | 112 | 113 | if __name__ == "__main__": 114 | main() 115 | -------------------------------------------------------------------------------- /Garmr/corechecks.py: -------------------------------------------------------------------------------- 1 | from urlparse import urlparse, urljoin 2 | import requests 3 | from scanner import ActiveTest, PassiveTest, HtmlTest, Scanner 4 | 5 | class HttpOnlyAttributePresent(PassiveTest): 6 | description = "Inspect the Set-Cookie: header and determine if the HttpOnly attribute is present." 7 | def analyze(self, response): 8 | cookieheader = "Set-Cookie" 9 | has_cookie = cookieheader in response.headers 10 | if has_cookie: 11 | if "httponly" in response.headers[cookieheader].lower(): 12 | result = self.result("Pass", "HttpOnly is set", response.headers[cookieheader]) 13 | else: 14 | result = self.result("Fail", "HttpOnly is not set", response.headers[cookieheader]) 15 | else: 16 | result = self.result("Skip", "No cookie is set by this response.", None) 17 | return result 18 | 19 | class SecureAttributePresent(PassiveTest): 20 | description = "Inspect the Set-Cookie: header and determine if the Secure attribute is present." 21 | def analyze(self, response): 22 | url = urlparse(response.url) 23 | cookieheader = "Set-Cookie" 24 | has_cookie = cookieheader in response.headers 25 | if has_cookie: 26 | if "secure" in response.headers[cookieheader].lower(): 27 | if url.scheme == "https": 28 | result = self.result("Pass", "Secure cookie attribute is set", response.headers[cookieheader]) 29 | else: 30 | result = self.result("Fail", "Secure cookie attribute should only be set for cookies sent over SSL.", response.headers[cookieheader]) 31 | else: 32 | if url.scheme == "https": 33 | result = self.result("Fail", "Secure cookie attribute is not set", response.headers[cookieheader]) 34 | else: 35 | result = self.result("Pass", "The secure attribute is not set (expected for HTTP)", response.headers[cookieheader]) 36 | else: 37 | result = self.result("Skip", "No cookie is set by this response.", None) 38 | return result 39 | 40 | 41 | class StrictTransportSecurityPresent(PassiveTest): 42 | secure_only = True 43 | description = "Check if the Strict-Transport-Security header is present in TLS requests." 44 | def analyze(self, response): 45 | stsheader = "Strict-Transport-Security" 46 | sts = stsheader in response.headers 47 | if sts == False: 48 | result = self.result("Fail", "Strict-Transport-Security header not found.", None) 49 | else: 50 | result = self.result("Pass", "Strict-Transport-Security header present.", response.headers[stsheader]) 51 | return result 52 | 53 | class XFrameOptionsPresent(PassiveTest): 54 | description = "Check if X-Frame-Options header is present." 55 | def analyze(self, response): 56 | xfoheader = "X-Frame-Options" 57 | xfo = xfoheader in response.headers 58 | if xfo == False: 59 | result = self.result("Fail", "X-Frame-Options header not found.", None) 60 | else: 61 | result = self.result("Pass", "X-Frame-Options header present.", response.headers[xfoheader]) 62 | return result 63 | 64 | class Http200Check(ActiveTest): 65 | run_passives = True 66 | description = "Make a GET request to the specified URL, reporting success only on a 200 response without following redirects" 67 | def do_test(self, url): 68 | sess = self.sessions[self.url] 69 | response = sess.get(url, allow_redirects=False) 70 | if response.status_code == 200: 71 | result = self.result("Pass", "The request returned an HTTP 200 response.", None) 72 | else: 73 | result = self.result("Fail", "The response code was %s" % response.status_code, None) 74 | return (result, response) 75 | 76 | class WebTouch(ActiveTest): 77 | run_passives = True 78 | description = "Make a GET request to the specified URL, and check for a 200 response after resolving redirects." 79 | def do_test(self, url): 80 | sess = self.sessions[self.url] 81 | response = sess.get(url) 82 | if response.status_code == 200: 83 | result = self.result("Pass", "The request returned an HTTP 200 response.", None) 84 | else: 85 | result = self.result("Fail", "The response code was %s" % response.status_code, None) 86 | return (result, response) 87 | 88 | 89 | class StsPresentCheck(ActiveTest): 90 | insecure_only = False 91 | run_passives = True 92 | description = "Inspect the second response in the Strict-Transport-Security redirect process according to http://tools.ietf.org/html/draft-hodges-strict-transport-sec" 93 | events = {} 94 | def do_test(self, url): 95 | stsheader = "Strict-Transport-Security" 96 | #XXX hack: we should take response isntead 97 | url = url.replace('http:', 'https:') 98 | #XXX end of hack 99 | sess = self.sessions[self.url] 100 | response = sess.get(url, allow_redirects=False) 101 | if stsheader in response.headers: 102 | result = self.result('Pass', 'Subsequential HTTPS Response for STS contained corresponding STS header', None) 103 | else: 104 | result = self.result('Fail', 'Subsequential HTTPS Response did not contain STS header', None) 105 | return (result, response) 106 | 107 | class StsRedirectCheck(ActiveTest): 108 | insecure_only = True 109 | run_passives = True 110 | description = "Inspect the first response in the Strict-Transport-Security redirect process according to http://tools.ietf.org/html/draft-hodges-strict-transport-sec" 111 | events = { "Pass": StsPresentCheck, 112 | "Error": None, 113 | "Fail": None } 114 | 115 | def do_test(self, url): 116 | stsheader = "Strict-Transport-Security" 117 | u = urlparse(url) 118 | if u.scheme == "http": 119 | sess = self.sessions[self.url] 120 | response = sess.get(url, allow_redirects=False) 121 | invalid_header = stsheader in response.headers 122 | is_redirect = response.status_code == 301 123 | bad_redirect = False 124 | if is_redirect == True: 125 | redirect = response.headers['location'] 126 | r = urlparse(redirect) #XXX do we need to check for same-domain? see sts draft! 127 | if r.scheme != 'https': 128 | pass 129 | else: 130 | bad_redirect = True 131 | 132 | #continue w/ Pass to see if next location contains stsheader? 133 | next_test = (invalid_header == False) and (is_redirect == True) and (bad_redirect == False) 134 | if next_test == True: 135 | message = "The STS upgrade occurs properly (no STS header on HTTP, a 301 redirect, and an STS header in the subsequent request." 136 | else: 137 | message = "%s%s%s" % ( 138 | "The initial HTTP response included an STS header (RFC violation)." if invalid_header else "", 139 | "" if is_redirect else "The initial HTTP response should be a 301 redirect (RFC violation see ).", 140 | "The 301 location must use the https scheme." if bad_redirect else "" 141 | ) 142 | result = self.result('Pass' if next_test else 'Fail', message, None) 143 | return (result, response) 144 | 145 | else: 146 | #XXX maybe just /change/ the scheme to enforce checking? 147 | result = self.result('Skip', 'Not checking for STS-Upgrade on already-secure connection', None) 148 | return result, None 149 | 150 | 151 | 152 | class CSPHeaderCheck(ActiveTest): 153 | # please revise after another readthrough of https://wiki.mozilla.org/Security/CSP/Specification#Sample_Policy_Definitions necessary 154 | insecure_only = False 155 | run_passives = True 156 | description = "Checks if the CSP Header is present and links to a policy. If it does, we will forward to another test to check if it present" 157 | def do_test(self, url): 158 | cspheader = "Content-Security-Policy" 159 | csproheader = 'Content-Security-Policy-Report-Only' 160 | sess = self.sessions[self.url] 161 | response = sess.get(url, allow_redirects=False) 162 | if cspheader in response.headers or csproheader in response.headers: 163 | result = self.result('Pass', 'CSP Header present.', None) 164 | else: 165 | result = self.result('Fail', 'No %s or %s in headers' % (cspheader, csproheader), None) 166 | return (result, response) 167 | 168 | 169 | class HttpsLoginForm(HtmlTest): 170 | description = "Check that html forms with password-type inputs point to https" 171 | def analyze_html(self, response, soup): 172 | url = urlparse(response.url) 173 | forms = soup.findAll('form') 174 | # look only at those form elements that have password type input elements as children 175 | forms = filter(lambda x: x.findChildren("input", type="password") ,forms) 176 | if len(forms) == 0: 177 | result = self.result("Skip", "There are no login forms on this page", None) 178 | return result 179 | failforms = [] 180 | for form in forms: 181 | if url.scheme == "https": 182 | if form['action'].startswith('http:'): 183 | failforms.append(form) 184 | else: 185 | if not form['action'].startswith('https'): 186 | failforms.append(form) 187 | if len(failforms) == 0: 188 | result = self.result("Pass", "All login forms point to secure resources", forms) 189 | else: 190 | result = self.result("Fail", "There are login forms pointing to insecure locations", failforms) 191 | return result 192 | 193 | 194 | class HttpsResourceOnHttpsLink(HtmlTest): 195 | # also called 'mixed content' 196 | description = "Check if all external resources are pointing to https links, when on https page" 197 | secure_only = True 198 | def analyze_html(self, response, soup): 199 | ''' there is a list on stackoverflow[1] which claims to contain 200 | all possible attributes hat may carry a URL. is 201 | there a way to confirm this list is exhaustive? 202 | I have removed attributes which are just links/pointers, 203 | we only want those attributes to resources, the browser 204 | downloads automatically 205 | [1] http://stackoverflow.com/questions/2725156/complete-list-of-html-tag-attributes-which-have-a-url-value/2725168#2725168 206 | ''' 207 | attrlist = ['codebase', 'background', 'src', 'usemap', 'data', 'icon', 'manifest', 'poster', 'archive'] 208 | failtags = [] 209 | for tag in soup.findAll(True): 210 | for attr in attrlist: 211 | if tag.has_key(attr): 212 | val = tag[attr] 213 | if val.startswith('http:'): 214 | failtags.append(tag) 215 | if len(failtags) == 0: 216 | result = self.result("Pass", "All external resources are https", None) 217 | else: 218 | result = self.result("Fail", "There are links to insecure locations", failtags) 219 | return result 220 | 221 | class InlineJS(HtmlTest): 222 | description = "complain about inline JS to improve migration to CSP" 223 | def analyze_html(self, response, soup): 224 | url = urlparse(response.url) 225 | scripts = soup.findAll('script') 226 | if len(scripts) == 0: 227 | result = self.result ("Skip", "There are no script tags.", None) 228 | return result 229 | inlinescripts = filter(lambda x: len(x.text) > 0, scripts) 230 | if len(inlinescripts) == 0: 231 | result = self.result("Pass", "No inline JavaScript found", None) 232 | else: 233 | result = self.result("Fail", "Inline JavaScript found", inlinescripts) 234 | return result 235 | 236 | 237 | def configure(scanner): 238 | if isinstance(scanner, Scanner) == False: 239 | raise Exception("Cannot configure a non-scanner object!") 240 | scanner.register_check(Http200Check) 241 | scanner.register_check(WebTouch) 242 | scanner.register_check(StrictTransportSecurityPresent) 243 | scanner.register_check(XFrameOptionsPresent) 244 | scanner.register_check(StsRedirectCheck) 245 | scanner.register_check(HttpOnlyAttributePresent) 246 | scanner.register_check(SecureAttributePresent) 247 | scanner.register_check(HttpsLoginForm) 248 | scanner.register_check(HttpsResourceOnHttpsLink) 249 | scanner.register_check(InlineJS) 250 | scanner.register_check(CSPHeaderCheck) 251 | -------------------------------------------------------------------------------- /Garmr/scanner.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from reporter import Reporter 3 | from urlparse import urlparse 4 | from BeautifulSoup import BeautifulSoup 5 | import ConfigParser 6 | import logging 7 | import requests 8 | import socket 9 | import traceback 10 | from inspect import getargspec, getmodule 11 | import subprocess 12 | import json 13 | 14 | 15 | 16 | def exec_helper(cmd, args=None): 17 | '''Use this function to call helpers, not to perform checks! 18 | The example use-case would a Testcase that required to get an 19 | authentication tokenout of a mailbox to complete a login procedure. 20 | The email-fetching would be done as a helper 21 | ''' 22 | if not args: 23 | params = cmd 24 | else: 25 | params = [cmd] + args # becomes [cmd, arg1, arg2] 26 | try: 27 | output = subprocess.Popen(params, stdout=subprocess.PIPE).communicate()[0] # use Popen instead of subprocess.get_output for Python2.6 compatibility 28 | try: 29 | res = json.loads(output) 30 | except ValueError: 31 | return {"result":"Fail", "message":"Invalid JSON data", "data":""} 32 | if 'result' in res and 'message' in res and 'data' in res: 33 | return res 34 | else: 35 | return {"result":"Fail", "message":"Incomplete JSON data. Your helper should return a dict with the keys result, message and data.", "data":""} 36 | except subprocess.CalledProcessError as e: # raised when? for Popen? 37 | return {"result":"Fail", "message":"The helper script returned with a non-zero returnvalue", "data": e.output} 38 | 39 | class PassiveTest(): 40 | secure_only = False 41 | insecure_only = False 42 | 43 | def analyze(self, response, results): 44 | return None 45 | 46 | def result(self, state, message, data): 47 | return {'state' : state, 'message' : message, 'data' : data } 48 | 49 | 50 | class ActiveTest(): 51 | new_session = False # enable (e.g. from cli) to enforce new session generation 52 | secure_only = False 53 | insecure_only = False 54 | run_passives = True 55 | description = "The base class for an Active Test." 56 | sessions = {} 57 | 58 | def __init__(self): 59 | if hasattr(self, "setup"): 60 | self.setup() 61 | 62 | #def get_url(self, url, status = True): 63 | # try: 64 | # sess = self.sessions[self.url] 65 | # except KeyError: 66 | # sess = requests.session() 67 | # #print "Issue request towards %s using %s" % (url, sess.cookies) 68 | # r = sess.get(url, allow_redirects = False) 69 | # print url, r.status_code, status 70 | # if status: 71 | # r.raise_for_status() 72 | # return r 73 | 74 | def execute(self, url, predecessor=None): 75 | self.url = url 76 | if self.url not in self.sessions or self.new_session: 77 | self.sessions[url] = requests.session() # Create per-target session 78 | try: 79 | if "pred" in getargspec(self.do_test).args: 80 | resulttuple = self.do_test(url, predecessor) 81 | else: 82 | resulttuple = self.do_test(url) 83 | except Exception, e: 84 | tb = traceback.format_exc() 85 | resulttuple = (ActiveTest().result("Error", e, tb), None) 86 | 87 | return resulttuple 88 | 89 | def result(self, state, message, data): 90 | return { 'state' : state, 'message' : message, 'data' : data, 'passive' : {}} 91 | 92 | class HtmlTest(PassiveTest): 93 | description = 'allow easy analysis of html source code' 94 | def analyze(self, response): 95 | if 'text/html' in response.headers['content-type']: 96 | soup = BeautifulSoup(response.content) 97 | return self.analyze_html(response, soup) 98 | else: 99 | result = self.result("Skip", "Content-type is not html "+ response.headers['content-type'], None) 100 | return result 101 | 102 | def analyze_html(self, response, soup): 103 | """ implement this method in subclass""" 104 | pass 105 | 106 | class Scanner(): 107 | logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s') 108 | logger = logging.getLogger("Garmr-Scanner") 109 | logger.setLevel(logging.DEBUG) 110 | 111 | def __init__(self): 112 | self.resolve_target = True 113 | self.force_passives = False 114 | self._disabled_tests_ = [] 115 | self._passive_tests_ = [] 116 | self._active_tests_ = [] 117 | self._finished_active_tests_ = [] 118 | self._targets_ = [] 119 | self._protos_ = ["http", "https"] 120 | Scanner.logger.debug("Scanner initialized.") 121 | self.reporter = Reporter() 122 | self.modules = [] 123 | 124 | def do_passive_scan(self, passiveclass, is_ssl, response): 125 | if passiveclass.secure_only and not is_ssl: 126 | Scanner.logger.debug("\t\t[%s] Skip Test invalid for http scheme" % passiveclass) 127 | passive_result = PassiveTest().result("Skip", "This check is only applicable to SSL requests.", None) 128 | start = datetime.now() 129 | passive_result['start'] = start 130 | passive_result['end'] = start 131 | passive_result["duration"] = 0 132 | else: 133 | start = datetime.now() 134 | passive = passiveclass() 135 | passive_result = passive.analyze(response) 136 | end = datetime.now() 137 | td = end - start 138 | passive_result['start'] = start 139 | passive_result['end'] = end 140 | passive_result['duration'] = float((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6)) / 10**6 141 | Scanner.logger.info("\t\t[%s] %s %s" % (passiveclass, passive_result['state'], passive_result['message'])) 142 | return passive_result 143 | 144 | def do_active_scan(self, testclass, is_ssl, target): 145 | ''' instantiate the class and run it against the specified target, if applicable ''' 146 | if (testclass.secure_only and not is_ssl): 147 | Scanner.logger.info("\t[Skip] [%s] (reason: secure_only)" % testclass) 148 | result = ActiveTest().result("Skip", "This check is only applicable to SSL requests", None) 149 | result['start'] = datetime.now() 150 | result['end'] = result['start'] 151 | result['duration'] = 0 152 | return result 153 | elif (testclass.insecure_only and is_ssl): 154 | Scanner.logger.info("\t[Skip] [%s] (reason: insecure_only)" % testclass) 155 | result = ActiveTest().result("Skip", "This check is only applicable to SSL requests", None) 156 | result['start'] = datetime.now() 157 | result['end'] = result['start'] 158 | result['duration'] = 0 159 | return result 160 | elif str(testclass).split('.')[-1] in self._disabled_tests_: 161 | Scanner.logger.info("\t[Skip] [%s] (reason: disabled)" % testclass) 162 | result = ActiveTest().result("Skip", "This check was marked as disabled.", None) 163 | result['start'] = datetime.now() 164 | result['end'] = result['start'] 165 | result['duration'] = 0 166 | return result 167 | start = datetime.now() 168 | test = testclass() # from now on we have an instance of the class 169 | if "pred" in getargspec(test.do_test).args: 170 | # Check if class accepts this parameter. avoids rewriting. 171 | predecessor_key = "%s.%s" % (getmodule(self._finished_active_tests_[-1]).__name__, self._finished_active_tests_[-1].__name__) 172 | predecessor_results = self.results[predecessor_key] 173 | result, response = test.execute(target, predecessor=predecessor_results) 174 | else: 175 | result, response = test.execute(target) 176 | end = datetime.now() 177 | td = end - start 178 | result['response'] = response 179 | result['start'] = start 180 | result['end'] = end 181 | result['duration'] = float((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6)) / 10**6 182 | Scanner.logger.info("\t[%s] %s %s" % (testclass, result['state'], result['message'])) 183 | self.reporter.write_active(testclass, result) 184 | if (result['state'] == "Error"): 185 | Scanner.logger.error(result['data']) 186 | if response != None and test.run_passives: 187 | result['passive'] = {} 188 | self.reporter.start_passives() 189 | for passive_testclass in self._passive_tests_: 190 | passive_testclass_key = "%s.%s" % (getmodule(passive_testclass).__name__, passive_testclass.__name__) 191 | result["passive"][passive_testclass_key] = self.do_passive_scan(passive_testclass, is_ssl, response) 192 | self.reporter.write_passive(passive_testclass, result["passive"][passive_testclass_key]) 193 | self.reporter.end_passives() 194 | return result 195 | 196 | def scan_target(self, target): 197 | ''' iterate over registered tests and deligate for scan ''' 198 | self.reporter.write_target(target) 199 | Scanner.logger.info("[%s] scanning:" % target) 200 | url = urlparse(target) 201 | is_ssl = url.scheme == "https" 202 | self.results = {} 203 | self.reporter.start_actives() 204 | self.active_tests_stack = self._active_tests_ 205 | while len(self.active_tests_stack) > 0: 206 | testclass = self.active_tests_stack[0] 207 | self.active_tests_stack = self.active_tests_stack[1:] 208 | testclass_key = "%s.%s" % (getmodule(testclass).__name__, testclass.__name__) 209 | self.results[testclass_key] = self.do_active_scan(testclass, is_ssl, target) 210 | if hasattr(testclass, 'events'): #TODO enforce every test to have event dict present? 211 | events_lower = dict([(k.lower(),v) for k,v in testclass.events.items()]) 212 | if self.results[testclass_key]['state'].lower() in events_lower and events_lower[self.results[testclass_key]['state'].lower()] != None: 213 | nexttest = events_lower[self.results[testclass_key]['state'].lower()] 214 | Scanner.logger.info("\t[%s] Instantiated because %s declares it as its successor (the event was '%s')" % (nexttest, testclass, self.results[testclass_key]['state'])) 215 | self.active_tests_stack.append(nexttest) # we have to hand over the response!!1, # important: we hand over an instance, not the class 216 | self._finished_active_tests_.append(testclass) 217 | self.reporter.end_actives() 218 | return self.results 219 | 220 | def run_scan(self): 221 | ''' iterate over target and deligate to list of tests ''' 222 | results = {} 223 | self.reporter.start_report() 224 | self.reporter.start_targets() 225 | if len(self._targets_) == 0: 226 | Scanner.logger.error('No targets configured.') 227 | return 228 | for target in self._targets_: 229 | try: 230 | results[target] = self.scan_target(target) 231 | except: 232 | Scanner.logger.error(traceback.format_exc()) 233 | self.reporter.end_targets() 234 | result = self.reporter.end_report(results) 235 | if (self.output == "-"): 236 | print result 237 | else: 238 | file = open(self.output, "w") 239 | file.write(self.reporter.end_report(results)) 240 | file.close() 241 | 242 | 243 | def register_target(self, url): 244 | ''' add target to the scanning engine ''' 245 | u = urlparse(url) 246 | valid = u.netloc != "" and u.scheme in self._protos_ 247 | reason = "%s%s" % ("[bad netloc]" if u.netloc == "" else "", "" if u.scheme in self._protos_ else "[bad scheme]") 248 | 249 | # todo - support ipv6 urls 250 | host = u.netloc.split(':')[0] 251 | if (self.resolve_target): 252 | try: 253 | socket.getaddrinfo(host, None) 254 | except socket.gaierror: 255 | valid = False 256 | reason = "%s[dns]" % reason 257 | else: 258 | valid = True 259 | if valid: 260 | self._targets_.append(url) 261 | Scanner.logger.debug("[target]: %s" % url) 262 | return 263 | Scanner.logger.error("%s is not a valid target (reason: %s)" % (url, reason)) 264 | 265 | def configure_check(self, check_name, key, value): 266 | if check_name in map(lambda x: str(x), self._active_tests_): 267 | index = map(lambda x: str(x), self._active_tests_).index(check_name) 268 | check = self._active_tests_[index] 269 | if check_name in map(lambda x: str(x), self._passive_tests_): 270 | index = map(lambda x: str(x), self._active_tests_).index(check_name) 271 | check = self._active_tests_[index] 272 | else: 273 | raise Exception("The requested check is not available (%s)" % check_name) 274 | if hasattr(check, "config") == False: 275 | raise Exception("This check cannot be configured.") 276 | if check.config.has_key(key) == False: 277 | raise Exception("%s is not a valid configuration for %s", key, check_name) 278 | check.config[key] = value 279 | Scanner.logger.debug("\t%s.%s=%s" % (check_name, key, value)) 280 | 281 | def disable_check(self, check_name): 282 | ''' add a previously added test to a blacklist of test that are to be skipped ''' 283 | if check_name in map(lambda x: str(x).split('.')[-1], self._active_tests_) or check_name in map(lambda x: str(x).split('.')[-1], self._passive_tests_): 284 | self._disabled_tests_.append(check_name) 285 | Scanner.logger.debug("\t%s disabled.", check_name) 286 | else: 287 | print "The requested check is not available (%s)" % check_name 288 | print "The list of available checks is %s for actives and %s for passives" % (map(lambda x: str(x).split('.')[-1], self._active_tests_), map(lambda x: str(x).split('.')[-1], self._passive_tests_)) 289 | Scanner.logger.debug("\t%s NOT disabled, because it could not be found.", check_name) 290 | 291 | def register_check(self, test): 292 | ''' add a test to the scanner ''' 293 | module = test.__module__ 294 | 295 | if module not in self.modules: 296 | self.modules.append(module) 297 | 298 | if hasattr(test, "execute"): 299 | self._active_tests_.append( test) 300 | Scanner.logger.debug("Added %s to active tests." % test) 301 | return len(self._active_tests_) 302 | if hasattr(test, "analyze"): 303 | self._passive_tests_.append( test) 304 | Scanner.logger.debug("Added %s to passive tests." % test) 305 | return len(self._passive_tests_) 306 | raise Exception('test is not a valid test type') 307 | 308 | def save_configuration(self, path): 309 | pass #XXX defunct 310 | # write out a configuration file. 311 | config = ConfigParser.RawConfigParser() 312 | config.add_section("Garmr") 313 | config.set("Garmr", "force-passives", self.force_passives) 314 | config.set("Garmr", "module", ", ".join(self.modules)) 315 | config.set("Garmr", "reporter", self.reporter.__class__) 316 | config.set("Garmr", "output", self.output) 317 | config.set("Garmr", "dns", self.resolve_target) 318 | 319 | if len(self._targets_) > 0: 320 | config.add_section("Targets") 321 | for i,target in enumerate(self._targets_): 322 | config.set("Targets", "%s"%i, target) 323 | 324 | for index, check in enumerate(self._active_tests_): 325 | check = str(check) 326 | config.add_section(check) 327 | if check not in self._disabled_tests_: 328 | config.set(check, "enabled", True) 329 | else: 330 | config.set(check, "enabled", False) 331 | if hasattr(self._active_tests_[index], "config"): 332 | for key in self._active_tests_[index].config.keys(): 333 | config.set(check, key, self._active_tests_[index].config[key]) 334 | 335 | for index, check in enumerate(self._passive_tests_): 336 | check = str(check) 337 | config.add_section(str(check)) 338 | if check not in self._disabled_tests_: 339 | config.set(check, "enabled", True) 340 | else: 341 | config.set(check, "enabled", False) 342 | if hasattr(self._passive_tests_[index], "config"): 343 | for key in self._passive_tests_[index].config.keys(): 344 | config.set(check, key, self._passive_tests_[index].config[key]) 345 | 346 | 347 | with open(path, 'w') as configfile: 348 | config.write(configfile) 349 | 350 | 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------