├── .gitignore ├── README.md ├── fortify ├── __init__.py ├── externalmetadata.py ├── fpr.py ├── fvdl.py ├── issue.py ├── project.py └── utils.py ├── fprstats.py ├── requirements.txt ├── rulemetadataexample.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python ### 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # PyBuilder 55 | target/ 56 | 57 | # Temporary files 58 | *.swp 59 | .DS_Store 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Overview 3 | ======== 4 | 5 | This is a utility to parse Fortify FPR files and generate meaningful output that can be used in automated processes or reports. 6 | 7 | The summary statistics can print out just the vulnerability counts so you can do things like flag apps that have > 0 critical or high vulnerabilities. 8 | 9 | The vulnerability summaries output can be used to send to developers who may not have HPE Fortify Auditworkbench or access to the Fortify SSC UI (e.g. vendors/contractors). It could also be used as input to a script to auto-assign vulnerabilities to dev teams. Or it could just let you pivot around the vulnerability statistics in an application. 10 | 11 | Fortify SSC UI has a REST interface now that may be useful instead of this tool, although it may be far slower for large projects than just parsing the FPR file. This utility also works offline if you have copies of FPR files already downloaded. 12 | 13 | About FPR Files 14 | ----- 15 | 16 | Fortify FPR files are just zip archives with various XML files inside. The parsing of the FPR file for this utility was mostly reverse-engineering and comparing to the Fortify results. I was able to get Fortify tech support to provide some of the calculations for the derived values that are used to calculate the criticality (aka Fortify Priority Order) though. 17 | 18 | There is a secret keyboard combination in Auditworkbench *COMMAND + OPTION + SHIFT + F* that provides a dump of the known attributes of a vulnerability instance that you can use for filters. 19 | 20 | Limitations 21 | ---- 22 | 23 | This utility currently implements very limited support for Fortify filtering syntax so won't generate counts for all but a couple of filter scenarios. Would be nice to add more use cases at some point. The FilterQuery class implements evaluation logic of the query and supports only: 24 | 25 | * Substring match 26 | * Negated substring match 27 | 28 | e.g. "Any vulnerability instance with a category containing the word Path" 29 | 30 | ``` 31 | category:Path 32 | ``` 33 | 34 | or "Any vulnerability instance that is not marked as Exploitable" 35 | 36 | ``` 37 | analysis:!Exploitable 38 | ``` 39 | 40 | This utility also doesn't support custom filter sets that can vastly change the vulnerability visibility and classifications. It just uses the Default filter set in the FPR. Would not be difficult to expose the ability to specify a non-default filter set, perhaps even by the string name. 41 | 42 | Tips 43 | ---- 44 | 45 | Watch out if you are using this to process FPR files generated directly from a scan before they have been uploaded to Fortify SSC. The vulnerability counts you see will not likely match up to what is on the Fortify SSC server due to: 46 | 47 | * Not having the same Project Template applied to the generated FPR as is applied and enforced on the Fortify SSC server (this can change visibility/filtering of vulnerabilities, reclassify vulnerabilities to different folders, different filter sets, etc. that all can result in different vulnerability counts) 48 | * Not having any of the auditing information available or suppression information that could change the vulnerability counts. 49 | 50 | Installation 51 | ============ 52 | 53 | Install to a user directory on OSX: 54 | ```bash 55 | python setup.py install --user --prefix= 56 | ``` 57 | 58 | Using the command-line utility 59 | ================ 60 | 61 | Usage help 62 | --------- 63 | 64 | ``` 65 | usage: Print statistics from a Fortify FPR file [-h] -f FPR [-p] [-c] [-s] 66 | [--high_priority_only] [-v] 67 | 68 | optional arguments: 69 | -h, --help show this help message and exit 70 | -f FPR, --file FPR generate stats for FPR 71 | -p, --project_info print project and scan info 72 | -c, --vuln_counts print vulnerabilities as CSV output 73 | -s, --vuln_summaries print vulnerability details as CSV output 74 | --high_priority_only For vulnerability summaries: Filters only High 75 | Priority relevant issues, which includes Critical/High 76 | and excludes anything suppressed, removed, hidden, NAI 77 | -v, --verbose print verbose/debug output 78 | ``` 79 | 80 | Print out vulnerability counts for an FPR 81 | ------ 82 | 83 | ```bash 84 | $ fprstats.py -f ~/Downloads/MyApp.fpr 85 | Got [108] issues, [0] hidden, [0] NAI, [0] Suppressed, [0] Removed 86 | ``` 87 | 88 | Print out vulnerability counts as CSV (machine-readable) format 89 | ------ 90 | 91 | ```bash 92 | $ fprstats.py -f ~/Downloads/MyApp.fpr -c 93 | Got [108] issues, [0] hidden, [0] NAI, [0] Suppressed, [0] Removed 94 | Critical, High, Medium, Low 95 | 0, 15, 0, 93 96 | ``` 97 | 98 | Print a report containing vulnerability summaries as CSV format 99 | ----- 100 | 101 | ```bash 102 | $ fprstats.py -f ~/Downloads/MyApp.fpr -s 103 | Got [108] issues, [0] hidden, [0] NAI, [0] Suppressed, [0] Removed 104 | file_line,path,id,kingdom,type_subtype,severity,nai,filtered,suppressed,removed 105 | MyService.java:100,src/main/java/com/example/www/myapp/services/MyService.java,1BE7DEE63734F7EC117948FACE57A977,Errors,Poor Error Handling: Overly Broad Throws,Low,False,V,False,False 106 | .... 107 | ``` 108 | 109 | You can redirect the CSV outputs to a file: 110 | 111 | ```bash 112 | $ fprstats.py -f ~/Downloads/MyApp.fpr -c > /tmp/MyApp.csv 113 | Got [108] issues, [0] hidden, [0] NAI, [0] Suppressed, [0] Removed 114 | ``` 115 | 116 | MyApp.csv contains: 117 | 118 | ``` 119 | $ cat /tmp/MyApp.csv 120 | Critical, High, Medium, Low 121 | 0, 15, 0, 93 122 | ``` 123 | 124 | Getting verbose log output to stderr 125 | ----- 126 | 127 | ```bash 128 | $ fprstats.py --high_priority_only -s -f ~/Downloads/MyApp.fpr -v > ~/Downloads/expweb-high-priority.csv 129 | ``` 130 | 131 | Would generate output like: 132 | 133 | ``` 134 | 2016-10-24 14:58:08,689 fortify.utils DEBUG Parsing audit.xml w/parser 135 | 2016-10-24 14:58:08,941 fortify.utils DEBUG Parsing filtertemplate.xml w/parser 136 | 2016-10-24 14:58:08,942 fortify.utils DEBUG Parsing audit.fvdl w/parser 137 | 2016-10-24 14:58:27,547 fortify.utils DEBUG Done parsing files from FPR 138 | 2016-10-24 14:58:48,505 fortify.project DEBUG Getting Vulnerabilities from FVDL 139 | 2016-10-24 14:59:49,942 fortify.project DEBUG Getting Issues for project and setting suppressed and analysis data. 140 | 2016-10-24 14:59:49,942 fortify.project DEBUG Have to process 12345 issues. 141 | 2016-10-24 14:59:50,729 fortify.project DEBUG Getting information about removed issues 142 | Got [12345] issues, [1000] hidden, [10286] NAI, [59] Suppressed, [1000] Removed 143 | 2016-10-24 14:59:59,792 fortify.issue WARNING Issue ID [B8F6FFD4A133A695B0B3F5B229C6A070] Missing Impact: Password Management : Null Password 144 | ``` 145 | 146 | Using the module in another python application 147 | ================ 148 | ```python 149 | from fortify import ProjectFactory 150 | 151 | project = ProjectFactory.create_project("some/path/to/file.fpr") 152 | 153 | # Now, print vulnerability summaries, etc. 154 | project.print_vuln_counts() 155 | ``` 156 | -------------------------------------------------------------------------------- /fortify/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | fortify 4 | 5 | ''' 6 | from .fpr import FPR 7 | from .issue import Issue, RemovedIssue 8 | from .project import Project, ProjectFactory 9 | -------------------------------------------------------------------------------- /fortify/externalmetadata.py: -------------------------------------------------------------------------------- 1 | from lxml.etree import ElementNamespaceClassLookup 2 | from lxml.objectify import ObjectifyElementClassLookup, ElementMaker, ObjectifiedElement 3 | from lxml import objectify 4 | from fvdl import FortifyObjectifiedDataElement 5 | 6 | ExternalMetadataParser = objectify.makeparser(ns_clean=True, 7 | remove_blank_text=True, 8 | resolve_entities=False, 9 | strip_cdata=False) 10 | 11 | ExternalMetadataElementNamespaceClassLookup = ElementNamespaceClassLookup( 12 | ObjectifyElementClassLookup()) 13 | 14 | class ExternalMetadataPackElement(FortifyObjectifiedDataElement): 15 | 16 | metadata_name_shortcut_cache = {} 17 | 18 | @property 19 | def namespace_map(self): 20 | # lxml is really dumb with xml using default namespaces. You have to define a dummy namespace prefix to the 21 | # default namespace even though that prefix doesn't exist in the raw xml. Define a consistent map for all xpath 22 | return {'z':'xmlns://www.fortifysoftware.com/schema/externalMetadata'} 23 | 24 | # in goes a metadata name and out comes a list of shortcuts for that name 25 | def get_shortcuts_for_name(self, name): 26 | if name not in self.metadata_name_shortcut_cache: 27 | self.metadata_name_shortcut_cache[name] = self.xpath("./z:ExternalList[z:Name='%s']/z:Shortcut/text()" % name, 28 | namespaces=self.namespace_map) 29 | 30 | return self.metadata_name_shortcut_cache[name] 31 | 32 | 33 | EXTERNALMETADATA_NAMESPACE = ExternalMetadataElementNamespaceClassLookup.get_namespace("xmlns://www.fortifysoftware.com/schema/externalMetadata") 34 | 35 | EXTERNALMETADATA_NAMESPACE['ExternalMetadataPack'] = ExternalMetadataPackElement 36 | 37 | ExternalMetadataParser.set_element_class_lookup( 38 | ExternalMetadataElementNamespaceClassLookup) 39 | 40 | ExternalMetadataPack = ElementMaker( 41 | annotate=False, 42 | namespace='xmlns://www.fortifysoftware.com/schema/externalMetadata', 43 | nsmap={ 44 | None: 'xmlns://www.fortifysoftware.com/schema/externalMetadata' 45 | } 46 | ) -------------------------------------------------------------------------------- /fortify/fpr.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | fortify.fpr 4 | ~~~~~~~~~~~ 5 | 6 | ''' 7 | from .utils import openfpr 8 | 9 | 10 | class FPR(object): 11 | 12 | cache = {} 13 | 14 | def __init__(self, project, **kwargs): 15 | if isinstance(project, basestring): 16 | self._project = project = openfpr(project) 17 | elif isinstance(project, dict): 18 | self._project = project 19 | else: 20 | raise TypeError 21 | 22 | self.FVDL = project['audit.fvdl'].getroot() 23 | self.cache[self.FVDL] = list(self.FVDL.iter()) 24 | self.Audit = project['audit.xml'].getroot() 25 | self.cache[self.Audit] = list(self.Audit.iter()) 26 | 27 | self.FilterTemplate=None 28 | 29 | if 'filtertemplate.xml' in project: 30 | self.FilterTemplate = project['filtertemplate.xml'].getroot() 31 | #self.cache[self.FilterTemplate] = list(self.FilterTemplate.iter()) 32 | 33 | self.ExternalMetadata=None 34 | if 'externalmetadata.xml' in project: 35 | self.ExternalMetadata = project['externalmetadata.xml'].getroot() 36 | #self.cache[self.ExternalMetadata] = list(self.ExternalMetadata.iter()) 37 | -------------------------------------------------------------------------------- /fortify/fvdl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | fortify.fvdl 4 | ~~~~~~~~~~~~ 5 | 6 | ''' 7 | from lxml.etree import ElementNamespaceClassLookup 8 | from lxml.objectify import ElementMaker, ObjectifiedDataElement, \ 9 | ObjectifyElementClassLookup 10 | from lxml import objectify 11 | from dateutil import tz 12 | import arrow 13 | import datetime 14 | import dateutil.parser 15 | import uuid 16 | import re 17 | import logging 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | # https://stackoverflow.com/a/6849299/630705 22 | class lazyproperty(object): 23 | ''' 24 | meant to be used for lazy evaluation of an object attribute. 25 | property should represent non-mutable data, as it replaces itself. 26 | ''' 27 | 28 | def __init__(self,fget): 29 | self.fget = fget 30 | self.func_name = fget.__name__ 31 | 32 | def __get__(self,obj,cls): 33 | if obj is None: 34 | return None 35 | value = self.fget(obj) 36 | setattr(obj,self.func_name,value) 37 | return value 38 | 39 | AuditParser = objectify.makeparser(ns_clean=True, 40 | remove_blank_text=True, 41 | resolve_entities=False, 42 | strip_cdata=False) 43 | 44 | FilterTemplateParser = objectify.makeparser(ns_clean=True, 45 | remove_blank_text=True, 46 | resolve_entities=False, 47 | strip_cdata=False) 48 | 49 | FVDLParser = objectify.makeparser(ns_clean=True, 50 | remove_blank_text=True, 51 | resolve_entities=False, 52 | strip_cdata=False) 53 | 54 | AuditObjectifiedElementNamespaceClassLookup = ElementNamespaceClassLookup( 55 | ObjectifyElementClassLookup()) 56 | 57 | FVDLObjectifiedElementNamespaceClassLookup = ElementNamespaceClassLookup( 58 | ObjectifyElementClassLookup()) 59 | 60 | FilterTemplateObjectifiedElementNamespaceClassLookup = ElementNamespaceClassLookup( 61 | ObjectifyElementClassLookup()) 62 | 63 | 64 | class FortifyObjectifiedDataElement(ObjectifiedDataElement): 65 | def __repr__(self): 66 | return "".format(self.tag, id(self)) 67 | 68 | 69 | class FVDLElement(FortifyObjectifiedDataElement): 70 | def get_vulnerabilities(self): 71 | return self.Vulnerabilities.Vulnerability if hasattr(self.Vulnerabilities, 'Vulnerability') else [] 72 | 73 | class AuditElement(FortifyObjectifiedDataElement): 74 | 75 | issue_analysisInfo_lookup = {} 76 | 77 | # Build a lookup dictionary to speed up resolving the analysis lookups that otherwise take a considerable amount of time doing xpath 78 | def build_issue_analysis_lookup(self): 79 | for issue in self.IssueList.iter("{xmlns://www.fortify.com/schema/audit}Issue"): 80 | # The analysis tag ID depends on the project template but hard-coding 81 | # for now should be reasonably safe since this is the default tag ID for analysis issues. 82 | analysis = issue.find( 83 | './ns2:Tag[@id=\'87f2364f-dcd4-49e6-861d-f8d3f351686b\']/ns2:Value', namespaces={'ns2': 'xmlns://www.fortify.com/schema/audit'}) 84 | analysisInfo = {} 85 | analysisInfo['analysis'] = analysis.text if analysis is not None else None 86 | analysisInfo['suppressed'] = True if 'suppressed' in issue.attrib and issue.attrib['suppressed'] == 'true' else False 87 | 88 | self.issue_analysisInfo_lookup[issue.attrib['instanceId']] = analysisInfo 89 | 90 | def get_issue_analysis(self, instanceId): 91 | return self.issue_analysisInfo_lookup[instanceId] if instanceId in self.issue_analysisInfo_lookup else None 92 | 93 | class DateTimeElement(FortifyObjectifiedDataElement): 94 | def __repr__(self): 95 | return "".format(self.tag, id(self)) 96 | 97 | @property 98 | def date(self): 99 | return self.datetime.date() 100 | 101 | @property 102 | def time(self): 103 | return self.datetime.time() 104 | 105 | @property 106 | def datetime(self): 107 | try: 108 | return arrow.get(str(self)) 109 | except arrow.parser.ParserError: 110 | return arrow.get(dateutil.parser.parse(str(self))) 111 | 112 | 113 | class TimeStampElement(FortifyObjectifiedDataElement): 114 | @property 115 | def date(self): 116 | return datetime.date(*map(int, self.get('date').split('-'))) 117 | 118 | @property 119 | def time(self): 120 | return datetime.time(*map(int, self.get('time').split(':'))) 121 | 122 | @property 123 | def datetime(self): 124 | return arrow.get( 125 | datetime.datetime.combine(self.date, self.time), 126 | tzinfo=tz.tzlocal()) # use local timezone 127 | 128 | 129 | class UUIDElement(FortifyObjectifiedDataElement): 130 | @property 131 | def uuid(self): 132 | return uuid.UUID(str(self)) 133 | 134 | class RuleInfoElement(FortifyObjectifiedDataElement): 135 | 136 | rules = {} 137 | 138 | def _init(self): 139 | # build a quicker rule lookup to avoid lots of xpath queries 140 | for rule in self.iter("{xmlns://www.fortifysoftware.com/schema/fvdl}Rule"): 141 | self.rules[rule.attrib['id']] = rule 142 | 143 | def get_rule(self, ruleId): 144 | return self.rules[ruleId] if ruleId in self.rules else None 145 | 146 | class RuleElement(FortifyObjectifiedDataElement): 147 | @property 148 | def id(self): 149 | return self.attrib['id'] 150 | 151 | @lazyproperty 152 | def metadata(self): 153 | metadata = {} 154 | for group in self.MetaInfo.Group: 155 | metadata[group.attrib['name']] = group.text 156 | return metadata 157 | 158 | 159 | class VulnerabilityElement(FortifyObjectifiedDataElement): 160 | @property 161 | def InstanceID(self): 162 | return self.InstanceInfo.InstanceID 163 | 164 | 165 | class FilterQuery: 166 | # metadata_element is the value that the criteria applies to. Criteria is applied to the value of the metadata element. 167 | def __init__(self, fpr, metadata_element=None, criteria=None, raw_querytext=None): 168 | self._metadata_element_shortcuts = [] 169 | if raw_querytext is None: 170 | self._metadata_element = metadata_element 171 | self._criteria = criteria 172 | else: 173 | # split raw 174 | pieces = raw_querytext.split(':') 175 | self._metadata_element = re.sub('^\[|\]$', '', pieces[0]) 176 | self._criteria = pieces[1] 177 | 178 | # Fortify actually uses shortcut names prefixed with 179 | # In filtertemplate.xml, it would specify [OWASP Top 10 2013], where that corresponds to a Name in 180 | # externalmetadata.xml. But in the actual audit.fvdl file, they use altcategoryOWASP2013 as the attribute 181 | # value for lookup. This appears to be "altcategory" prefixing one of the Shortcut values from the 182 | # externalmetadata definitions: OWASP2013 So, we have to map one to the other for 183 | # lookups 184 | if fpr.ExternalMetadata is not None: 185 | metadata_element_shortcuts = fpr.ExternalMetadata.get_shortcuts_for_name(self._metadata_element) 186 | if len(metadata_element_shortcuts) > 0: 187 | # we found shortcuts for this name, which means it's a metadata category name. Store all variations for 188 | # matches in the future, prefixed with "altcategory" (but none have spaces, so excluding those) 189 | self._metadata_element_shortcuts = [] 190 | for s in metadata_element_shortcuts: 191 | if ' ' not in s: 192 | self._metadata_element_shortcuts.append("altcategory" + s) 193 | 194 | def _evaluate_one(self, metadata_element, metadata): 195 | # This understands a limited set of Fortify's query language. To really support this would take 196 | # more tests and reverse engineering perhaps and maybe a full blown syntax parser to do right 197 | is_filtered = False 198 | metadata_value = metadata.get(metadata_element, None) 199 | if metadata_value is not None: 200 | # parse the criteria and check the value against it. Quick and dirty for now. Supports substring match and 201 | # negated substring match 202 | negated = True if self._criteria.startswith('!') else False 203 | substring_to_find = self._criteria.replace('!', '') 204 | # contains = T, negated = F => T 205 | # contains = T, negated = T => F 206 | # contains = F, negated = F => F 207 | # contains = F, negated = T => T 208 | is_filtered = not ((metadata_value != 'None' and substring_to_find in metadata_value.lower()) and negated) 209 | 210 | return is_filtered 211 | 212 | def evaluate(self, metadata): 213 | is_filtered = False 214 | 215 | if len(self._metadata_element_shortcuts) > 0: 216 | # process metadata shortcuts, not the element itself 217 | for s in self._metadata_element_shortcuts: 218 | is_filtered = self._evaluate_one(s, metadata) 219 | if is_filtered: 220 | break 221 | else: 222 | is_filtered = self._evaluate_one(self._metadata_element, metadata) 223 | 224 | return is_filtered 225 | 226 | 227 | class FilterElement(FortifyObjectifiedDataElement): 228 | def get_filter_query(self, fpr): 229 | query_object = None 230 | # Not being able to have state is a really annoying limitation of lxml. We need to access externalmetadata here 231 | if self.action == 'hide': 232 | query_object = FilterQuery(raw_querytext=self.query.text, fpr=fpr) 233 | 234 | return query_object 235 | 236 | 237 | class FilterTemplateElement(FortifyObjectifiedDataElement): 238 | # determines whether an issue is hidden or not 239 | def is_hidden(self, fpr, issue): 240 | 241 | is_hidden = False 242 | 243 | # skip if we've already done this to be idempotent 244 | if self.default_filterset is not None: 245 | 246 | # find all hide filter criteria and configure the object so they are available 247 | filter_queries = [] 248 | hide_filters = self.default_filterset.xpath("./Filter[action = 'hide']") 249 | for f in hide_filters: 250 | filter_queries.append(f.get_filter_query(fpr)) 251 | 252 | for q in filter_queries: 253 | is_hidden = q.evaluate(issue.metadata) 254 | if is_hidden: 255 | break # found a condition that applies (multiple separate conditions are ORed together) 256 | 257 | return is_hidden 258 | 259 | @lazyproperty 260 | def default_filterset(self): 261 | # find the active FilterSet and get any rules that hide things 262 | # TODO: could allow caller to specify which filterset to use to toggle views of data 263 | default_filter_set = self.find(".//FilterSet[@enabled='true']") 264 | if default_filter_set is None: 265 | logger.warn("No default filterset found!") 266 | 267 | return default_filter_set 268 | 269 | 270 | AUDIT_NAMESPACE = AuditObjectifiedElementNamespaceClassLookup.get_namespace( 271 | 'xmlns://www.fortify.com/schema/audit') 272 | 273 | FVDL_NAMESPACE = FVDLObjectifiedElementNamespaceClassLookup.get_namespace( 274 | 'xmlns://www.fortifysoftware.com/schema/fvdl') 275 | 276 | FILTERTEMPLATE_NAMESPACE = FilterTemplateObjectifiedElementNamespaceClassLookup.get_namespace(None) 277 | 278 | AUDIT_NAMESPACE['Audit'] = AuditElement 279 | AUDIT_NAMESPACE['CreationDate'] = DateTimeElement 280 | AUDIT_NAMESPACE['EditTime'] = DateTimeElement 281 | AUDIT_NAMESPACE['RemoveScanDate'] = DateTimeElement 282 | AUDIT_NAMESPACE['Timestamp'] = DateTimeElement 283 | AUDIT_NAMESPACE['WriteDate'] = DateTimeElement 284 | 285 | FVDL_NAMESPACE['BeginTS'] = TimeStampElement 286 | FVDL_NAMESPACE['CreatedTS'] = TimeStampElement 287 | FVDL_NAMESPACE['EndTS'] = TimeStampElement 288 | FVDL_NAMESPACE['FVDL'] = FVDLElement 289 | FVDL_NAMESPACE['FirstEventTimestamp'] = TimeStampElement 290 | FVDL_NAMESPACE['ModifiedTS'] = TimeStampElement 291 | FVDL_NAMESPACE['UUID'] = UUIDElement 292 | FVDL_NAMESPACE['Vulnerability'] = VulnerabilityElement 293 | FVDL_NAMESPACE['Rule'] = RuleElement 294 | FVDL_NAMESPACE['RuleInfo'] = RuleInfoElement 295 | 296 | FILTERTEMPLATE_NAMESPACE['FilterTemplate'] = FilterTemplateElement 297 | FILTERTEMPLATE_NAMESPACE['Filter'] = FilterElement 298 | 299 | AuditParser.set_element_class_lookup( 300 | AuditObjectifiedElementNamespaceClassLookup) 301 | 302 | FVDLParser.set_element_class_lookup( 303 | FVDLObjectifiedElementNamespaceClassLookup) 304 | 305 | FilterTemplateParser.set_element_class_lookup( 306 | FilterTemplateObjectifiedElementNamespaceClassLookup) 307 | 308 | FVDL = ElementMaker( 309 | annotate=False, 310 | namespace='xmlns://www.fortifysoftware.com/schema/FVDL', 311 | nsmap={ 312 | None: 'xmlns://www.fortifysoftware.com/schema/FVDL', 313 | 'xsi': 'http://www.w3.org/2001/XMLSchema-instance' 314 | } 315 | ) 316 | 317 | Audit = ElementMaker( 318 | annotate=False, 319 | namespace='', 320 | nsmap={ 321 | None: 'xmlns://www.fortify.com/schema/AUDIT', 322 | 'xsi': 'http://www.w3.org/2001/XMLSchema-instance' 323 | } 324 | ) 325 | 326 | 327 | def parse(source, **kwargs): 328 | return objectify.parse(source, parser=FVDLParser, **kwargs) 329 | -------------------------------------------------------------------------------- /fortify/issue.py: -------------------------------------------------------------------------------- 1 | import os 2 | from decimal import * 3 | import logging 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | # object representing a Fortify issue 8 | class Issue: 9 | def __init__(self, iid, ruleid, kingdom, type, subtype): 10 | self.id = iid # instance ID 11 | self.ruleid = ruleid 12 | self.kingdom = kingdom 13 | self.type = type 14 | self.subtype = subtype 15 | self.suppressed = False 16 | self.metadata = {} 17 | 18 | # Factory method to create an instance from a vulnerability XML object directly 19 | @classmethod 20 | def from_vulnerability(cls, vulnerability): 21 | instance = cls(vulnerability.InstanceID, vulnerability.ClassInfo.ClassID, 22 | vulnerability.ClassInfo.Kingdom, vulnerability.ClassInfo.Type, 23 | vulnerability.ClassInfo.Subtype if hasattr(vulnerability.ClassInfo, 'Subtype') else None) 24 | instance._build_metadata(vulnerability) 25 | return instance 26 | 27 | # augments the metadata dictionary with additional metadata, such as rule metadata 28 | def add_metadata(self, rulemetadata): 29 | self.metadata.update(rulemetadata) 30 | # some of these have different case or strings in the XML so add equivalent versions that 31 | # Fortify uses for filters 32 | if 'Accuracy' in self.metadata: 33 | self.metadata['accuracy'] = Decimal(self.metadata['Accuracy']) 34 | if 'Impact' in self.metadata: 35 | self.metadata['impact'] = Decimal(self.metadata['Impact']) 36 | # Fortify only uses this it seems if the instance probability is not set 37 | if 'Probability' in self.metadata and 'probability' not in self.metadata: 38 | self.metadata['probability'] = Decimal(self.metadata['Probability']) 39 | if 'RemediationEffort' in self.metadata: 40 | self.metadata['remediation effort'] = Decimal(self.metadata['RemediationEffort']) 41 | 42 | @property 43 | def category(self): 44 | # returns a combination of type and subtype, or just type if that's all we have 45 | return self.type + ': ' + self.subtype if self.subtype is not None else self.type 46 | 47 | @property 48 | def analysis(self): 49 | return self.metadata['analysis'] if 'analysis' in self.metadata else None 50 | 51 | @analysis.setter 52 | def analysis(self, analysis): 53 | self.metadata['analysis'] = analysis 54 | 55 | @property 56 | def hidden(self): 57 | # TODO: determine who should own issue visibility, especially since that can change by filters 58 | return self.removed 59 | 60 | @property 61 | def removed(self): 62 | return 'analyzer' in self.metadata and self.metadata['analyzer'] == 'RemovedIssue' 63 | 64 | @property 65 | def suppressed(self): 66 | return self.metadata['suppressed'] == 'true' if 'suppress' in self.metadata else False 67 | 68 | @suppressed.setter 69 | def suppressed(self, suppressed): 70 | self.metadata['suppressed'] = str(suppressed).lower() 71 | 72 | # generate the metadata dictionary for the issue. Here is an example: 73 | def _build_metadata(self, vulnerability): 74 | # add vulnerability metadata 75 | # TODO: add more 76 | self.metadata['severity'] = Decimal(vulnerability.InstanceInfo.InstanceSeverity.pyval) 77 | self.metadata['confidence'] = Decimal(vulnerability.InstanceInfo.Confidence.pyval) 78 | if hasattr(vulnerability.InstanceInfo, 'MetaInfo'): 79 | # this probability takes precedence over rule probability 80 | prob = vulnerability.InstanceInfo.MetaInfo.find("./x:Group[@name='Probability']", namespaces={ 81 | 'x': 'xmlns://www.fortifysoftware.com/schema/fvdl'}) 82 | if prob is not None: 83 | self.metadata['probability'] = Decimal(prob.pyval) 84 | 85 | # /f:FVDL/f:Vulnerabilities/f:Vulnerability[2]/f:AnalysisInfo/f:Unified/f:Context 86 | if hasattr(vulnerability.AnalysisInfo.Unified, "Trace") and hasattr( 87 | vulnerability.AnalysisInfo.Unified.Trace.Primary.Entry, "Node"): 88 | # This is more consistent with what Fortify shows, if available 89 | child = vulnerability.AnalysisInfo.Unified.Trace.Primary.Entry.Node.SourceLocation 90 | self.metadata['file'] = child.attrib['path'] 91 | if 'shortfile' not in self.metadata: 92 | self.metadata['shortfile'] = os.path.basename(child.attrib['path']) 93 | if 'line' not in self.metadata: 94 | self.metadata['line'] = child.attrib['line'] 95 | elif hasattr(vulnerability.AnalysisInfo.Unified, 'ReplacementDefinitions'): 96 | child = vulnerability.AnalysisInfo.Unified.ReplacementDefinitions 97 | for thisdef in child.Def: 98 | if thisdef.attrib['key'] == 'PrimaryLocation.file': 99 | self.metadata['shortfile'] = thisdef.attrib['value'] 100 | elif thisdef.attrib['key'] == 'PrimaryLocation.line': 101 | self.metadata['line'] = thisdef.attrib['value'] 102 | 103 | if hasattr(vulnerability.AnalysisInfo.Unified.Context, 'FunctionDeclarationSourceLocation'): 104 | child = vulnerability.AnalysisInfo.Unified.Context.FunctionDeclarationSourceLocation 105 | self.metadata['file'] = child.attrib['path'] 106 | if 'shortfile' not in self.metadata: 107 | self.metadata['shortfile'] = os.path.basename(child.attrib['path']) 108 | if 'line' not in self.metadata: 109 | self.metadata['line'] = child.attrib['line'] 110 | 111 | self.metadata['category'] = self.category 112 | self.metadata['type'] = self.type 113 | self.metadata['subtype'] = self.subtype 114 | 115 | if hasattr(vulnerability.AnalysisInfo.Unified.Context, 'Function'): 116 | child = vulnerability.AnalysisInfo.Unified.Context.Function 117 | # namespace not always populated for some reason 118 | self.metadata['package'] = child.attrib['namespace'] if 'namespace' in child.attrib else None 119 | self.metadata['class'] = child.attrib['enclosingClass'] if 'enclosingClass' in child.attrib else None 120 | elif hasattr(vulnerability.AnalysisInfo.Unified.Context, 'ClassIdent'): 121 | child = vulnerability.AnalysisInfo.Unified.Context.ClassIdent 122 | self.metadata['package'] = child.attrib['namespace'] if 'namespace' in child.attrib else None 123 | self.metadata['class'] = None 124 | else: 125 | # Fortify builds a package name even in this case. Not sure what data it uses from FVDL. 126 | self.metadata['package'] = None 127 | self.metadata['class'] = None 128 | 129 | def _likelihood(self): 130 | # This comes from Fortify support documentation 131 | # Likelihood = (Accuracy x Confidence x Probability) / 25 132 | likelihood = (self.metadata['accuracy'] * self.metadata['confidence'] * self.metadata['probability']) / 25 133 | return round(likelihood, 1) 134 | 135 | def is_NAI(self): 136 | return self.analysis == 'Not an Issue' 137 | 138 | @property 139 | def risk(self): 140 | # This calculates Fortify Priority Order, which actually uses other metadata to place vulnerabilities 141 | # into 1 of 4 quadrants of a grid based on thresholds as follows (from Fortify support documentation): 142 | # - 'Critical' if Impact >=2.5 && Likelihood >= 2.5. 143 | # - 'High' If Impact >=2.5 && Likelihood < 2.5. 144 | # - 'Medium' If Impact < 2.5 && Likelihood >= 2.5. 145 | # - 'Low' if impact < 2.5 && likelihood < 2.5. 146 | criticality = None 147 | 148 | if 'impact' in self.metadata: 149 | impact = self.metadata['impact'] 150 | likelihood = self._likelihood() 151 | 152 | if impact >= 2.5 and likelihood >= 2.5: 153 | # print "Rule ID [%s] Critical: impact [%d], likelihood [%d], accuracy [%d], confidence [%d], probability[%d]" % 154 | # (self.id, impact, self._likelihood(), self.metadata['accuracy'], self.metadata['confidence'], self.metadata['probability']) 155 | criticality = 'Critical' 156 | elif impact >= 2.5 > likelihood: 157 | criticality = 'High' 158 | elif impact < 2.5 <= likelihood: 159 | criticality = 'Medium' 160 | elif impact < 2.5 and likelihood < 2.5: 161 | criticality = 'Low' 162 | else: 163 | logger.warn("Issue ID [%s] Missing Impact: %s : %s" % (self.id, self.type, self.subtype)) 164 | 165 | return criticality 166 | 167 | @property 168 | def is_open_high_priority(self): 169 | # encapsulates the logic of whether a finding is open and high priority 170 | risk = self.risk 171 | pci_relevant = (risk == 'Critical' or risk == 'High') \ 172 | and not self.is_NAI() \ 173 | and not self.removed \ 174 | and not self.suppressed \ 175 | and not self.hidden 176 | return pci_relevant 177 | 178 | 179 | class RemovedIssue(Issue): 180 | @classmethod 181 | def from_auditxml(cls, removed): 182 | type_subtype = cls._split_type_subtype(removed.Category) 183 | instance = cls(removed.attrib['instanceId'], None, 184 | 'Unknown - Custom Issue', type_subtype[0], 185 | type_subtype[1] if len(type_subtype) == 2 else None) 186 | instance._build_removed_metadata(removed) 187 | return instance 188 | 189 | @classmethod 190 | def _split_type_subtype(cls, category): 191 | # removed issues have a single field with combined type/subtype (or not) so this splits those back out 192 | pieces = category.text.split(':') 193 | return pieces 194 | 195 | def _build_removed_metadata(self, removed): 196 | self.metadata['analyzer'] = 'RemovedIssue' 197 | self.metadata['category'] = self.category 198 | self.metadata['type'] = self.type 199 | self.metadata['subtype'] = self.subtype 200 | 201 | self.metadata['file'] = removed.File.text 202 | self.metadata['shortfile'] = os.path.basename(removed.File.text) 203 | self.metadata['line'] = removed.Line 204 | self.metadata['confidence'] = Decimal(removed.Confidence.pyval) 205 | self.metadata['severity'] = Decimal(removed.Severity.pyval) 206 | self.metadata['probability'] = Decimal(removed.Probability.pyval) 207 | self.metadata['accuracy'] = Decimal(removed.Accuracy.pyval) 208 | self.metadata['impact'] = Decimal(removed.Impact.pyval) 209 | -------------------------------------------------------------------------------- /fortify/project.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from . import FPR, Issue, RemovedIssue 3 | import sys 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | def eprint(*args, **kwargs): 9 | print(*args, file=sys.stderr, **kwargs) 10 | 11 | # configures fortify project objects 12 | class ProjectFactory: 13 | # creates a new project object by loading the FPR from fprpath and building necessary data structures 14 | def __init__(self): 15 | pass 16 | 17 | @staticmethod 18 | def create_project(fprpath): 19 | fpr = FPR(fprpath) 20 | 21 | project = Project(fpr) 22 | 23 | # find every vulnerability and model as an Issue object attached to the project 24 | logger.debug("Getting Vulnerabilities from FVDL") 25 | for vuln in fpr.FVDL.get_vulnerabilities(): 26 | issue = Issue.from_vulnerability(vuln) 27 | 28 | rule = fpr.FVDL.EngineData.RuleInfo.get_rule(vuln.ClassInfo.ClassID) 29 | if rule is not None and hasattr(rule, 'metadata'): 30 | issue.add_metadata(rule.metadata) 31 | 32 | # now, we need to apply visibility rules from the filtertemplate, if one exists, for the 33 | if fpr.FilterTemplate is not None: 34 | issue.hidden = fpr.FilterTemplate.is_hidden(fpr, issue) 35 | 36 | project.add_or_update_issue(issue) 37 | 38 | # now, associate the analysis info with the issues we know about. 39 | # Only FPRs with audit information will have this to associate. 40 | logger.debug("Getting Issues for project and setting suppressed and analysis data.") 41 | issues = project.get_issues() 42 | logger.debug("Have to process %d issues." % len(issues)) 43 | # build lookup 44 | fpr.Audit.build_issue_analysis_lookup() 45 | for issueid in issues: 46 | 47 | i = project.get_issue(issueid) 48 | analysisInfo = fpr.Audit.get_issue_analysis(issueid) 49 | 50 | if analysisInfo is not None: 51 | # set suppressed status 52 | i.suppressed = analysisInfo['suppressed'] 53 | if analysisInfo['analysis'] is not None: 54 | i.analysis = analysisInfo['analysis'] 55 | 56 | project.add_or_update_issue(i) # add it back in to replace the previous one 57 | 58 | # now, add information about removed issues 59 | logger.debug("Getting information about removed issues") 60 | if hasattr(fpr.Audit, 'IssueList') and hasattr(fpr.Audit.IssueList, 'RemovedIssue'): 61 | for removed in fpr.Audit.IssueList.RemovedIssue: 62 | ri = RemovedIssue.from_auditxml(removed) 63 | project.add_or_update_issue(ri) 64 | 65 | removedissues = [i for i in issues.values() if i.removed] 66 | suppressedissues = [i for i in issues.values() if i.suppressed] 67 | hiddenissues = [i for i in issues.values() if i.hidden] 68 | naiissues = [i for i in issues.values() if i.is_NAI()] 69 | eprint("Got [%d] issues, [%d] hidden, [%d] NAI, [%d] Suppressed, [%d] Removed" % (len(issues), len(hiddenissues), len(naiissues), len(suppressedissues), len(removedissues))) 70 | 71 | return project # A fortify project, containing one or more issues, with metadata 72 | 73 | 74 | class Project: 75 | def __init__(self, fpr): 76 | self._fpr = fpr 77 | self._issues = {} 78 | 79 | # set project properties 80 | if hasattr(fpr.Audit.ProjectInfo, 'Name'): 81 | self.ProjectName=fpr.Audit.ProjectInfo.Name 82 | else: 83 | self.ProjectName=None 84 | 85 | if hasattr(fpr.Audit.ProjectInfo, 'ProjectVersionId'): 86 | self.ProjectVersionId=fpr.Audit.ProjectInfo.ProjectVersionId 87 | else: 88 | self.ProjectVersionId=None 89 | 90 | for loc in fpr.FVDL.Build.LOC: 91 | if loc.attrib['type'] == 'Fortify': 92 | self.ScannedELOC=loc.text 93 | elif loc.attrib['type'] == 'Line Count': 94 | self.ScannedLOC=loc.text 95 | 96 | def add_or_update_issue(self, issue): 97 | if issue.id in self._issues: 98 | # remove first and decrement counts, if change in severity 99 | current = self._issues[issue.id] 100 | if issue != current: 101 | # unless this is a new object, nothing to do 102 | del self._issues[issue.id] 103 | 104 | # add the issue to the list, if necessary 105 | self._issues[issue.id] = issue 106 | 107 | def get_issues(self): 108 | return self._issues 109 | 110 | def get_issue(self, id): 111 | return self._issues[id] 112 | 113 | def print_project_info(self): 114 | # TODO: print an overview of the project information (name, etc.) and scan information 115 | return 116 | 117 | def print_vuln_counts(self): 118 | vuln_counts = {'Critical': 0, 119 | 'High': 0, 120 | 'Medium': 0, 121 | 'Low': 0, 122 | } 123 | for i in self._issues.values(): 124 | # exclude hidden, NAI and suppressed (TODO: could be configurable) 125 | if not (i.hidden or i.is_NAI() or i.suppressed): 126 | if i.risk is None: 127 | logger.warn("Risk calculation error for issue [%s]" % i.id) 128 | else: 129 | vuln_counts[i.risk] += 1 130 | 131 | print("Critical, High, Medium, Low") 132 | print("%d, %d, %d, %d" % (vuln_counts['Critical'], vuln_counts['High'], vuln_counts['Medium'], vuln_counts['Low'])) 133 | 134 | def print_vuln_summaries(self, open_high_priority): 135 | # TODO: enable sorting by severity and file_line by default. 136 | print("file_line,path,id,kingdom,type_subtype,severity,nai,filtered,suppressed,removed,analysis") 137 | for i in self._issues.itervalues(): 138 | if not open_high_priority or i.is_open_high_priority: 139 | print("%s:%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" % \ 140 | (i.metadata['shortfile'], i.metadata['line'], i.metadata['file'], i.id, i.kingdom, i.category, i.risk, i.is_NAI(), "H" if i.hidden else "V", i.suppressed, i.removed, i.analysis)) 141 | 142 | def get_fpr(self): 143 | return self._fpr 144 | -------------------------------------------------------------------------------- /fortify/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | fortify.utils 4 | ~~~~~~~~~~~~~ 5 | 6 | ''' 7 | import os 8 | from lxml import objectify 9 | from zipfile import ZipFile 10 | import logging 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | from .fvdl import AuditParser, FilterTemplateParser, FVDLParser 15 | from .externalmetadata import ExternalMetadataParser 16 | 17 | 18 | XML_PARSERS = { 19 | 'audit.fvdl': FVDLParser, 20 | 'audit.xml': AuditParser, 21 | 'filtertemplate.xml': FilterTemplateParser, 22 | 'ExternalMetadata/externalmetadata.xml': ExternalMetadataParser 23 | } 24 | 25 | 26 | def openfpr(fprfile): 27 | ''' 28 | Read and parse important files from an FPR. 29 | 30 | :param fprfile: Path to the FPR file, or a file-like object. 31 | :returns: A dict of :class:`lxml.etree._ElementTree` objects. 32 | ''' 33 | 34 | zfpr = fprfile 35 | 36 | if not isinstance(fprfile, ZipFile): 37 | zfpr = ZipFile(fprfile) 38 | 39 | pkg = {} 40 | 41 | for filename in (f for f in zfpr.namelist() if f in XML_PARSERS): 42 | parser = XML_PARSERS.get(filename) 43 | artifact = zfpr.open(filename) 44 | logger.debug("Parsing %s w/parser %r", filename, parser) 45 | # index by filename only, not folder 46 | filename = os.path.basename(filename) 47 | pkg[filename] = objectify.parse(artifact, parser=parser) 48 | 49 | logger.debug("Done parsing files from FPR") 50 | return pkg 51 | -------------------------------------------------------------------------------- /fprstats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import argparse 3 | import logging 4 | 5 | from fortify import ProjectFactory 6 | 7 | parser = argparse.ArgumentParser("Print statistics from a Fortify FPR file") 8 | parser.add_argument("-f", "--file", dest="fprfile", required=True, 9 | help="generate stats for FPR", metavar="FPR") 10 | parser.add_argument("-p", "--project_info", default=False, 11 | action="store_true", dest="print_project_info", 12 | help="print project and scan info") 13 | parser.add_argument("-c", "--vuln_counts", 14 | action="store_true", dest="print_vuln_counts", default=False, 15 | help="print vulnerabilities as CSV output") 16 | parser.add_argument("-s", "--vuln_summaries", 17 | action="store_true", dest="print_vuln_summaries", default=False, 18 | help="print vulnerability details as CSV output") 19 | parser.add_argument("--high_priority_only", 20 | action="store_true", dest="print_high_priority_only", default=False, 21 | help="For vulnerability summaries: Filters only High Priority relevant issues, which includes Critical/High and excludes anything suppressed, removed, hidden, NAI") 22 | parser.add_argument("-v", "--verbose", dest="verbose", required=False, 23 | action="store_true", help="print verbose/debug output") 24 | 25 | args = parser.parse_args() 26 | 27 | # create console handler with a higher log level 28 | logLevel = logging.DEBUG if args.verbose else logging.ERROR 29 | #logging.basicConfig(level=logLevel,format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s') 30 | logger = logging.getLogger(__name__) 31 | logger.setLevel(logLevel) 32 | consoleLogger = logging.StreamHandler() 33 | consoleLogger.setLevel(logLevel) 34 | consoleLogger.setFormatter(logging.Formatter(fmt='%(asctime)s %(name)-12s %(levelname)-8s %(message)s')) 35 | 36 | # add console logger to the root logger to cover all module loggers 37 | rootLogger = logging.getLogger() 38 | rootLogger.addHandler(consoleLogger) 39 | rootLogger.setLevel(logLevel) 40 | 41 | project = ProjectFactory.create_project(args.fprfile) 42 | 43 | if args.print_project_info: 44 | project.print_project_info() 45 | 46 | if args.print_vuln_counts: 47 | project.print_vuln_counts() 48 | 49 | if args.print_vuln_summaries: 50 | project.print_vuln_summaries(args.print_high_priority_only) 51 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml>=3.4.0 2 | arrow>=0.4.4 3 | -------------------------------------------------------------------------------- /rulemetadataexample.txt: -------------------------------------------------------------------------------- 1 | abstract,, 2 | accuracy,1.0, 3 | analysis,Not an Issue, 4 | analysis type,SCA, 5 | analyzer,Control Flow, 6 | attack payload,, 7 | attack type,, 8 | audience,broad, 9 | audited,true, 10 | body,, 11 | bug,E6F3D946CAD4226CA48DE35055D353D2#MyService.java:169:169, 12 | category,XML External Entity Injection, 13 | category_source_type,MAPPED, 14 | class,DukeService, 15 | comments,, 16 | confidence,5.0, 17 | context_id,, 18 | cookies,, 19 | correlated,, 20 | correlation group,, 21 | cwe,CWE ID 611, 22 | description,, 23 | file,MyService.java, 24 | fisma,SI, 25 | fortify priority order,High, 26 | functionname,unmarshal, 27 | headers,, 28 | history,, 29 | http version,, 30 | impact,4.0, 31 | instance id,78919A4DDE4B42CE396923DCE010EEE8, 32 | issue age,"Issue New: Aug 3, 2015", 33 | issue state,Not an Issue, 34 | issue_analysis_history,, 35 | kingdom,Input Validation and Representation, 36 | likelihood,0.6, 37 | line,169.0, 38 | manual,, 39 | mapped category,, 40 | method,, 41 | min_virtual_call_confidence,1.0, 42 | nist sp 800-53 rev.4,SI-10 Information Input Validation (P1), 43 | owasp mobile 2014,M4 Unintended Data Leakage, 44 | owasp top 10 2004,A6 Injection Flaws, 45 | owasp top 10 2007,A2 Injection Flaws, 46 | owasp top 10 2010,A1 Injection, 47 | owasp top 10 2013,A1 Injection, 48 | package,net.axley.impl, 49 | parameters,, 50 | pci 1.1,Requirement 6.5.6, 51 | pci 1.2,"Requirement 6.3.1.1, Requirement 6.5.2", 52 | pci 2.0,Requirement 6.5.1, 53 | pci 3.0,Requirement 6.5.1, 54 | primary context,net.axley.impl.MyService.unmarshal, 55 | primaryrule,0DE6DCC8-25D6-4224-AE1B-B7F8C024AB95, 56 | probability,3.0, 57 | remediation effort,1.0, 58 | replacement_store,null, 59 | request id,, 60 | response,, 61 | rta protected,Not Protected, 62 | sans top 25 2009,, 63 | sans top 25 2010,, 64 | sans top 25 2011,, 65 | secondary requests,, 66 | severity,4.0, 67 | sink,transformerFactory.newTransformer() : XML document parsed allowing external entity resolution, 68 | source,, 69 | source context,, 70 | sourcefile,, 71 | sourceline,, 72 | status,Reviewed, 73 | stig 3.1,APP3510 CAT I, 74 | stig 3.4,"APP3510 CAT I, APP3810 CAT I", 75 | stig 3.5,"APP3510 CAT I, APP3810 CAT I", 76 | stig 3.6,"APP3510 CAT I, APP3810 CAT I", 77 | stig 3.7,"APP3510 CAT I, APP3810 CAT I", 78 | stig 3.9,"APP3510 CAT I, APP3810 CAT I", 79 | subtype,, 80 | taint,, 81 | trace,, 82 | trigger,, 83 | type,XML External Entity Injection, 84 | url,, 85 | wasc 2.00,XML External Entities (WASC-43), 86 | wasc 24 + 2,, 87 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='Python-Fortify', 6 | version='1.0', 7 | description='Python FPR Library', 8 | author='Marcin Wielgoszewski, Jason Axley', 9 | author_email='jason@axley.net', 10 | url='https://github.com/jaxley/python-fortify', 11 | packages=['fortify'], 12 | scripts=['fprstats.py'] 13 | ) 14 | --------------------------------------------------------------------------------