diff --git a/Pipfile b/Pipfile index 696e130..344f78c 100644 --- a/Pipfile +++ b/Pipfile @@ -7,7 +7,7 @@ verify_ssl = true [packages] colorama = ">=0.4.5" -libsast = ">=3.1.7" +libsast = ">=3.1.8" semgrep = "==1.172.0" sarif-om = ">=1.0.4" jschema-to-python = ">=1.2.3" diff --git a/Pipfile.lock b/Pipfile.lock index b1ddee8..3f7cee8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "579ae981222a3cb09de2a1607a025b6e4f72ae1e123d191ba100ac8ec12af599" + "sha256": "d63d1c7f14a3d770e2e657177df05da593dc770dec48e2542b85370a7d31f3df" }, "pipfile-spec": 6, "requires": { @@ -467,12 +467,12 @@ }, "libsast": { "hashes": [ - "sha256:191002e6e9a6b9f81206bec2bf3bdea5c57034459a9537c02748e66a64650db6", - "sha256:6321d6d00a72b39aa7fadc7053ae84bc24681ffe4f6925cc8b6b2bb7c0635ad7" + "sha256:67b2d968b6cc23c6a0694856f9bae5d2cd8ca21bbf45d9f9346fffbfba1f35c0", + "sha256:94dadd4a8aa2447c477b2de4f3b58c39d16412a10f2416bde998517d9656e934" ], "index": "pypi", - "markers": "python_version >= '3.10'", - "version": "==3.1.7" + "markers": "python_version >= '3.10' and python_version < '4.0'", + "version": "==3.1.8" }, "markdown-it-py": { "hashes": [ diff --git a/README.md b/README.md index c9bb97c..7f82ca0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # mobsfscan -**mobsfscan** is a static analysis tool that can find insecure code patterns in your Android and iOS source code. Supports Java, Kotlin, Android XML, Swift and Objective C Code. mobsfscan uses [MobSF](https://github.com/MobSF/Mobile-Security-Framework-MobSF) static analysis rules and is powered by [semgrep](https://github.com/returntocorp/semgrep) and [libsast](https://github.com/ajinabraham/libsast) pattern matcher. +**mobsfscan** is a static analysis tool that can find insecure code patterns in your Android and iOS source code. Supports Java, Kotlin, Android XML, iOS Info.plist, Swift and Objective C Code. mobsfscan uses [MobSF](https://github.com/MobSF/Mobile-Security-Framework-MobSF) static analysis rules and is powered by [semgrep](https://github.com/returntocorp/semgrep) and [libsast](https://github.com/ajinabraham/libsast) pattern matcher. Made with ![Love](https://cloud.githubusercontent.com/assets/4301109/16754758/82e3a63c-4813-11e6-9430-6015d98aeaab.png) in India [![Tweet](https://img.shields.io/twitter/url?url=https://github.com/MobSF/mobsfscan)](https://twitter.com/intent/tweet/?text=mobsfscan%20is%20a%20static%20analysis%20tool%20that%20can%20find%20insecure%20code%20patterns%20in%20your%20Android%20and%20iOS%20source%20code.%20Supports%20Java,%20Kotlin,%20Swift,%20and%20Objective%20C%20Code.%20by%20%40ajinabraham%20%40OpenSecurity_IN&url=https://github.com/MobSF/mobsfscan) @@ -31,8 +31,9 @@ Requires Python 3.10–3.14 ```bash $ mobsfscan -usage: mobsfscan [-h] [--json] [--sarif] [--sonarqube] [--html] [--type {android,ios,auto}] - [-o OUTPUT] [-c CONFIG] [-mp {default,billiard,thread}] [-w] [--no-fail] [-v] +usage: mobsfscan [-h] [--json] [--sarif] [--sonarqube] [--gitlab-sast] [--html] + [--type {android,ios,auto}] [-o OUTPUT] [-c CONFIG] + [-mp {default,billiard,thread}] [-w] [--no-fail] [-v] [path ...] positional arguments: @@ -42,7 +43,8 @@ options: -h, --help show this help message and exit --json set output format as JSON --sarif set output format as SARIF 2.1.0 - --sonarqube set output format compatible with SonarQube + --sonarqube set output format as SonarQube generic issues (10.3+) + --gitlab-sast set output format as GitLab SAST report --html set output format as HTML --type {android,ios,auto} optional: force android or ios rules explicitly @@ -214,10 +216,17 @@ A `.mobsf` file in the root of the source code directory allows you to configure severity-filter: - WARNING - ERROR + + severity-overrides: + ios_log: ERROR + android_logging: WARNING ``` + +`severity-overrides` changes the reported severity for specific rule IDs (`INFO`, `WARNING`, or `ERROR`). Overrides are applied before `severity-filter` and affect CLI output, exit codes, and report formats (SARIF, SonarQube, GitLab SAST). + ## Suppress Findings -You can suppress findings from source files by adding the comment `// mobsf-ignore: rule_id1, rule_id2` to the line that trigger the findings. +You can suppress findings from source files by adding the comment `// mobsf-ignore: rule_id1, rule_id2` on the line that triggers the finding. Only that match is suppressed; other matches of the same rule in the file still report. Example: @@ -246,8 +255,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 - - uses: actions/setup-python@v5.3.0 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: '3.12' - name: mobsfscan @@ -272,10 +281,14 @@ jobs: mobsfscan: runs-on: ubuntu-latest name: mobsfscan code scanning + permissions: + security-events: write + actions: read + contents: read steps: - name: Checkout the code - uses: actions/checkout@v4.2.2 - - uses: actions/setup-python@v5.3.0 + uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: '3.12' - name: mobsfscan @@ -283,7 +296,7 @@ jobs: with: args: '. --sarif --output results.sarif || true' - name: Upload mobsfscan report - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v4 with: sarif_file: results.sarif ``` @@ -295,16 +308,37 @@ Add the following to the file `.gitlab-ci.yml`. ```yaml stages: - - test + - test + mobsfscan: - image: python - before_script: - - pip3 install --upgrade mobsfscan - script: - - mobsfscan . + image: python:3.12 + stage: test + before_script: + - pip3 install --upgrade mobsfscan + script: + - mobsfscan . --gitlab-sast -o gl-sast-report.json + artifacts: + reports: + sast: gl-sast-report.json +``` + +Example command (local): + +```bash +mobsfscan . --gitlab-sast -o gl-sast-report.json +``` + +This writes a native [GitLab SAST report](https://docs.gitlab.com/user/application_security/sast/) so findings appear in the Vulnerability Report / MR security widget without a SARIF converter. + +#### SonarQube / SonarCloud + +`--sonarqube` writes the [generic issue format](https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/importing-external-issues/generic-issue-import-format) (SonarQube 10.3+ / SonarCloud), with separate `rules` and `issues` arrays: + +```bash +mobsfscan . --sonarqube -o mobsfscan-sonar.json ``` -Example: +Import with `sonar.externalIssuesReportPaths=mobsfscan-sonar.json`. #### Travis CI @@ -327,7 +361,7 @@ version: 2.1 jobs: mobsfscan: docker: - - image: cimg/python:3.9.6 + - image: cimg/python:3.12 steps: - checkout - run: diff --git a/action.yml b/action.yml index 1cd3ff1..c89ab3f 100644 --- a/action.yml +++ b/action.yml @@ -15,7 +15,8 @@ inputs: -h, --help show this help message and exit --json set output format as JSON --sarif set output format as SARIF 2.1.0 - --sonarqube set output format compatible with SonarQube + --sonarqube set output format as SonarQube generic issues (10.3+) + --gitlab-sast set output format as GitLab SAST report --html set output format as HTML --type {android,ios,auto} optional: force android or ios rules explicitly diff --git a/mobsfscan/__init__.py b/mobsfscan/__init__.py index 10e46c2..6506188 100644 --- a/mobsfscan/__init__.py +++ b/mobsfscan/__init__.py @@ -6,7 +6,7 @@ __title__ = 'mobsfscan' __authors__ = 'Ajin Abraham' __copyright__ = f'Copyright {datetime.now().year} Ajin Abraham, OpenSecurity' -__version__ = '0.4.5' +__version__ = '1.0.0' __version_info__ = tuple(int(i) for i in __version__.split('.')) __all__ = [ '__title__', diff --git a/mobsfscan/__main__.py b/mobsfscan/__main__.py index 0d9451a..7f5e9a7 100644 --- a/mobsfscan/__main__.py +++ b/mobsfscan/__main__.py @@ -8,6 +8,7 @@ from mobsfscan.mobsfscan import MobSFScan from mobsfscan.formatters import ( cli, + gitlab_sast, json_fmt, sarif, sonarqube, @@ -42,7 +43,11 @@ def main(): help='set output format as SARIF 2.1.0', action='store_true') parser.add_argument('--sonarqube', - help='set output format compatible with SonarQube', + help=('set output format as SonarQube generic ' + 'issues (10.3+)'), + action='store_true') + parser.add_argument('--gitlab-sast', + help='set output format as GitLab SAST report', action='store_true') parser.add_argument('--html', help='set output format as HTML', @@ -78,7 +83,11 @@ def main(): action='store_true') args = parser.parse_args() if args.path: - is_json = args.json or args.sonarqube or args.sarif + is_json = ( + args.json + or args.sonarqube + or args.sarif + or args.gitlab_sast) scan_results = MobSFScan( args.path, is_json, @@ -91,6 +100,11 @@ def main(): args.output, scan_results, __version__) + elif args.gitlab_sast: + gitlab_sast.gitlab_sast_output( + args.output, + scan_results, + __version__) elif args.json: json_fmt.json_output( args.output, diff --git a/mobsfscan/formatters/gitlab_sast.py b/mobsfscan/formatters/gitlab_sast.py new file mode 100644 index 0000000..be1ec5a --- /dev/null +++ b/mobsfscan/formatters/gitlab_sast.py @@ -0,0 +1,205 @@ +# -*- coding: utf_8 -*- +"""GitLab SAST report formatter. + +Produces JSON conforming to GitLab's SAST report schema so findings can be +uploaded with artifacts:reports:sast (no SARIF converter required). + +See: https://docs.gitlab.com/development/integrations/secure/ +Schema: https://gitlab.com/gitlab-org/security-products/security-report-schemas +""" +from datetime import datetime, timezone +from hashlib import sha256 +from pathlib import PurePath +import json + +from mobsfscan.formatters.sarif import ( + _cwe_id, + format_rule_name, +) + +# Widely supported GitLab security-report schema version +SCHEMA_VERSION = '15.0.4' +TS_FORMAT = '%Y-%m-%dT%H:%M:%S' +SCANNER_URL = 'https://github.com/MobSF/mobsfscan' + + +def gitlab_severity(severity): + """Map mobsfscan severity to GitLab SAST severity.""" + return { + 'ERROR': 'Critical', + 'WARNING': 'Medium', + 'INFO': 'Info', + }.get((severity or '').upper(), 'Unknown') + + +def gitlab_confidence(severity): + return { + 'ERROR': 'High', + 'WARNING': 'High', + 'INFO': 'Medium', + }.get((severity or '').upper(), 'Unknown') + + +def _vuln_id(rule_id, file_path, start_line, end_line): + raw = f'{rule_id}|{file_path}|{start_line}|{end_line}' + return sha256(raw.encode('utf-8')).hexdigest() + + +def _relative_file(file_path): + if not file_path: + return '.' + pure = PurePath(file_path) + return pure.as_posix() + + +def _cwe_identifier(cwe): + cwe_id = _cwe_id(cwe) + if not cwe_id: + return None + num = cwe_id.split('-', 1)[1] + return { + 'type': 'cwe', + 'name': cwe_id, + 'value': num, + 'url': f'https://cwe.mitre.org/data/definitions/{num}.html', + } + + +def _named_identifier(id_type, value, name=None, url=None): + if not value: + return None + text = str(value).strip() + if not text: + return None + ident = { + 'type': id_type, + 'name': name or text, + 'value': text.split(':', 1)[0].strip(), + } + if url: + ident['url'] = url + return ident + + +def build_identifiers(rule_id, metadata): + """Build GitLab identifiers from rule id and MobSF metadata.""" + identifiers = [{ + 'type': 'mobsfscan_rule_id', + 'name': f'mobsfscan-{rule_id}', + 'value': rule_id, + }] + cwe = _cwe_identifier(metadata.get('cwe')) + if cwe: + identifiers.append(cwe) + owasp = _named_identifier( + 'owasp_mobile', + metadata.get('owasp-mobile'), + name=metadata.get('owasp-mobile')) + if owasp: + identifiers.append(owasp) + masvs = _named_identifier( + 'masvs', + metadata.get('masvs'), + name=metadata.get('masvs')) + if masvs: + identifiers.append(masvs) + return identifiers + + +def build_links(metadata): + ref = metadata.get('reference') or metadata.get('ref') + if not ref: + return [] + return [{'url': ref}] + + +def create_vulnerability(rule_id, issue_dict, file_item=None): + """Create one GitLab SAST vulnerability object.""" + meta = issue_dict.get('metadata') or {} + description = meta.get('description') or rule_id + name = format_rule_name(rule_id, meta) + + if file_item: + file_path = _relative_file(file_item.get('file_path')) + start_line = int(file_item.get('match_lines', [1, 1])[0] or 1) + end_line = int(file_item.get('match_lines', [1, 1])[1] or start_line) + else: + file_path = '.' + start_line = 1 + end_line = 1 + + vuln = { + 'id': _vuln_id(rule_id, file_path, start_line, end_line), + 'category': 'sast', + 'name': name, + 'message': name, + 'description': description, + 'severity': gitlab_severity(meta.get('severity')), + 'confidence': gitlab_confidence(meta.get('severity')), + 'scanner': { + 'id': 'mobsfscan', + 'name': 'mobsfscan', + }, + 'location': { + 'file': file_path, + 'start_line': start_line, + 'end_line': end_line, + }, + 'identifiers': build_identifiers(rule_id, meta), + } + links = build_links(meta) + if links: + vuln['links'] = links + return vuln + + +def build_vulnerabilities(scan_results): + vulnerabilities = [] + for rule_id, issue in (scan_results.get('results') or {}).items(): + files = issue.get('files') or [] + if not files: + vulnerabilities.append(create_vulnerability(rule_id, issue)) + continue + for file_item in files: + vulnerabilities.append( + create_vulnerability(rule_id, issue, file_item)) + return vulnerabilities + + +def build_scan(version, start_time, end_time, status='success'): + scanner = { + 'id': 'mobsfscan', + 'name': 'mobsfscan', + 'url': SCANNER_URL, + 'vendor': {'name': 'OpenSecurity'}, + 'version': version, + } + return { + 'analyzer': dict(scanner), + 'scanner': scanner, + 'type': 'sast', + 'start_time': start_time, + 'end_time': end_time, + 'status': status, + } + + +def gitlab_sast_output(outfile, scan_results, version): + """Write or print a GitLab SAST report.""" + now = datetime.now(timezone.utc).strftime(TS_FORMAT) + report = { + 'version': SCHEMA_VERSION, + 'vulnerabilities': build_vulnerabilities(scan_results), + 'scan': build_scan(version, now, now), + } + jout = json.dumps( + report, + sort_keys=True, + indent=2, + separators=(',', ': ')) + if outfile: + with open(outfile, 'w') as of: + of.write(jout) + else: + print(jout) + return jout diff --git a/mobsfscan/formatters/sarif.py b/mobsfscan/formatters/sarif.py index 4d237f8..c3f7b99 100644 --- a/mobsfscan/formatters/sarif.py +++ b/mobsfscan/formatters/sarif.py @@ -6,9 +6,11 @@ bandit_sarif_formatter/formatter.py MIT License, Copyright (c) Microsoft Corporation. +Enriched for GitHub Code Scanning, GitLab SARIF import, and SonarQube. """ from datetime import datetime, timezone -from pathlib import PurePath +from pathlib import Path, PurePath +import re import urllib.parse as urlparse import sarif_om as om @@ -16,6 +18,11 @@ from jschema_to_python.to_json import to_json TS_FORMAT = '%Y-%m-%dT%H:%M:%SZ' +_CWE_ID_RE = re.compile(r'(?i)\bCWE-?(\d+)\b') +_MAX_RULE_NAME = 255 +_MAX_TAGS = 10 +_DEFAULT_HELP = ('https://mobile-security.gitbook.io/' + 'mobile-security-testing-guide/') def level_from_severity(severity): @@ -23,19 +30,117 @@ def level_from_severity(severity): 'ERROR': 'error', 'WARNING': 'warning', 'INFO': 'note', - }.get(severity, 'none') + }.get((severity or '').upper(), 'none') + + +def security_severity_score(metadata=None): + """GitHub/GitLab security-severity (0.1-10.0). Prefer CVSS when present.""" + metadata = metadata or {} + cvss = metadata.get('cvss') + if cvss is not None: + try: + score = float(cvss) + except (TypeError, ValueError): + # Non-numeric CVSS in rule metadata; use severity map below. + score = None + if score is not None and 0.1 <= score <= 10.0: + return f'{score:.1f}' + return { + 'ERROR': '9.0', + 'WARNING': '5.5', + 'INFO': '2.0', + }.get((metadata.get('severity') or '').upper(), '5.0') + + +def precision_from_severity(severity): + return { + 'ERROR': 'high', + 'WARNING': 'high', + 'INFO': 'medium', + }.get((severity or '').upper(), 'medium') def to_uri(file_path): + """Prefer repo-relative paths for GHAS/GitLab; fall back to path/URI.""" pure_path = PurePath(file_path) if pure_path.is_absolute(): - return pure_path.as_uri() + try: + rel = Path(file_path).resolve().relative_to(Path.cwd().resolve()) + return urlparse.quote(rel.as_posix()) + except (ValueError, OSError): + return pure_path.as_uri() + return urlparse.quote(pure_path.as_posix()) + + +def _cwe_id(cwe): + """Return normalized CWE-NNN id from metadata, if present.""" + if not cwe: + return None + match = _CWE_ID_RE.search(str(cwe).split(':', 1)[0]) + if not match: + return None + return f'CWE-{match.group(1)}' + + +def _first_sentence(text): + """Return a short title from the first sentence of description.""" + if not text: + return '' + cleaned = ' '.join(str(text).strip().split()) + for sep in ('. ', '? ', '! '): + if sep in cleaned: + cleaned = cleaned.split(sep, 1)[0] + break else: - return urlparse.quote(pure_path.as_posix()) + cleaned = cleaned.rstrip('.?!') + return cleaned.strip() + + +def _slug_tag(prefix, value): + """Build a compact tag like owasp-mobile/m3 from expanded or raw values.""" + if not value: + return None + raw = str(value).split(':', 1)[0].strip().lower() + raw = raw.replace('mstg-', '').replace('masvs-', '') + raw = re.sub(r'[^a-z0-9\-]+', '-', raw).strip('-') + if not raw: + return None + return f'{prefix}/{raw}' + + +def build_tags(metadata=None): + """Build SARIF tags (max 10 for GitLab) from available metadata.""" + metadata = metadata or {} + tags = ['security'] + cwe_id = _cwe_id(metadata.get('cwe')) + if cwe_id: + tags.append(f'external/cwe/{cwe_id.lower()}') + for prefix, key in ( + ('owasp-mobile', 'owasp-mobile'), + ('masvs', 'masvs')): + tag = _slug_tag(prefix, metadata.get(key)) + if tag and tag not in tags: + tags.append(tag) + return tags[:_MAX_TAGS] + + +def format_rule_name(rule_id, metadata=None): + """Build a human-readable SARIF rule name for dashboards. + Prefers the check description (unique per rule) and appends the CWE id + when available. Capped at 255 chars for GitLab. + """ + metadata = metadata or {} + title = _first_sentence(metadata.get('description')) + if not title: + title = ''.join(word.capitalize() for word in rule_id.split('_')) -def format_rule_name(rule_id): - return ''.join(word.capitalize() for word in rule_id.split('_')) + cwe_id = _cwe_id(metadata.get('cwe')) + if cwe_id and cwe_id not in title.upper(): + title = f'{title} ({cwe_id})' + if len(title) > _MAX_RULE_NAME: + title = title[:_MAX_RULE_NAME - 1].rstrip() + '…' + return title def add_results(path, scan_results, run): @@ -57,16 +162,29 @@ def add_results(path, scan_results, run): def create_rule_results(path, rule_id, issue_dict, rules, rule_indices): rule_results = [] rule, rule_index = rules.get(rule_id), rule_indices.get(rule_id) - ref_url = ('https://mobile-security.gitbook.io/' - 'mobile-security-testing-guide/') if not rule: - doc = issue_dict['metadata'].get('reference') or ref_url - cwe_id = issue_dict['metadata']['cwe'].split(':')[0].lower() + meta = issue_dict.get('metadata') or {} + doc = meta.get('reference') or meta.get('ref') or _DEFAULT_HELP + description = meta.get('description') or format_rule_name(rule_id, meta) + short_title = format_rule_name(rule_id, meta) + level = level_from_severity(meta.get('severity')) + help_text = description + if doc: + help_text = f'{description}\n\nReference: {doc}' rule = om.ReportingDescriptor( id=rule_id, - name=format_rule_name(rule_id), + name=short_title, + short_description=om.MultiformatMessageString(text=short_title), + full_description=om.MultiformatMessageString(text=description), + help=om.MultiformatMessageString(text=help_text), help_uri=doc, - properties={'tags': ['security', f'external/cwe/{cwe_id}']}) + default_configuration=om.ReportingConfiguration(level=level), + properties={ + 'tags': build_tags(meta), + 'precision': precision_from_severity(meta.get('severity')), + 'security-severity': security_severity_score(meta), + 'problem.severity': level if level != 'none' else 'warning', + }) rule_index = len(rules) rules[rule_id] = rule rule_indices[rule_id] = rule_index @@ -78,7 +196,7 @@ def create_rule_results(path, rule_id, issue_dict, rules, rule_indices): if not issue_dict.get('files'): default_location = om.Location( physical_location=om.PhysicalLocation( - artifact_location=om.ArtifactLocation(uri=path[0]), + artifact_location=om.ArtifactLocation(uri=to_uri(path[0])), region=om.Region( start_line=1, end_line=1, @@ -104,17 +222,20 @@ def create_location(item): def create_result(rule, rule_index, issue_dict, locations): + meta = issue_dict.get('metadata') or {} + score = security_severity_score(meta) return om.Result( rule_id=rule.id, rule_index=rule_index, - message=om.Message(text=issue_dict['metadata']['description']), - level=level_from_severity(issue_dict['metadata']['severity']), + message=om.Message(text=meta.get('description') or rule.name), + level=level_from_severity(meta.get('severity')), locations=locations, properties={ - 'owasp-mobile': issue_dict['metadata']['owasp-mobile'], - 'masvs': issue_dict['metadata']['masvs'], - 'cwe': issue_dict['metadata']['cwe'], - 'reference': issue_dict['metadata']['reference'], + 'owasp-mobile': meta.get('owasp-mobile'), + 'masvs': meta.get('masvs'), + 'cwe': meta.get('cwe'), + 'reference': meta.get('reference') or meta.get('ref'), + 'security-severity': score, }) diff --git a/mobsfscan/formatters/sonarqube.py b/mobsfscan/formatters/sonarqube.py index 93de845..85b479c 100644 --- a/mobsfscan/formatters/sonarqube.py +++ b/mobsfscan/formatters/sonarqube.py @@ -1,67 +1,134 @@ # -*- coding: utf_8 -*- -"""Sonarqube output format.""" +"""SonarQube generic issue format (SonarQube 10.3+). -from mobsfscan.formatters.json_fmt import json_output +See: +https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/importing-external-issues/generic-issue-import-format +""" +import json +from mobsfscan.formatters.sarif import format_rule_name -def get_sonarqube_issue(mobsfscan_issue): - sonarqube_severity_mapping = { - 'ERROR': 'CRITICAL', - 'WARNING': 'MAJOR', - 'INFO': 'INFO', + +SEVERITY_MAP = { + 'ERROR': 'CRITICAL', + 'WARNING': 'MAJOR', + 'INFO': 'INFO', +} + +IMPACT_SEVERITY_MAP = { + 'ERROR': 'HIGH', + 'WARNING': 'MEDIUM', + 'INFO': 'LOW', +} + + +def standard_severity(severity): + return SEVERITY_MAP.get((severity or '').upper(), 'MAJOR') + + +def impact_severity(severity): + return IMPACT_SEVERITY_MAP.get((severity or '').upper(), 'MEDIUM') + + +def build_rule(rule_id, issue_dict): + """Build a SonarQube generic-issue rule object.""" + meta = issue_dict.get('metadata') or {} + description = meta.get('description') or rule_id + severity = meta.get('severity') + return { + 'id': rule_id, + 'name': format_rule_name(rule_id, meta), + 'description': description, + 'engineId': 'mobsfscan', + 'cleanCodeAttribute': 'TRUSTWORTHY', + 'type': 'VULNERABILITY', + 'severity': standard_severity(severity), + 'impacts': [{ + 'softwareQuality': 'SECURITY', + 'severity': impact_severity(severity), + }], } - secondary_locations = [] - issue_data = mobsfscan_issue['metadata'] - # Handle missing controls - if not mobsfscan_issue.get('files'): + + +def build_locations(issue_dict): + """Return primary location and optional secondary locations.""" + meta = issue_dict.get('metadata') or {} + description = meta.get('description') or '' + files = issue_dict.get('files') or [] + + if not files: + primary = { + 'message': description, + 'filePath': '.', + 'textRange': { + 'startLine': 1, + 'endLine': 1, + }, + } + return primary, [] + + locations = [] + for file_item in files: + message = description + match_string = file_item.get('match_string') + if match_string: + message = f'{description} [{match_string}]' text_range = { - 'startLine': 1, - 'endLine': 0, + 'startLine': int(file_item['match_lines'][0]), + 'endLine': int(file_item['match_lines'][1]), } - location = { - 'message': issue_data['description'], - 'filePath': '', + match_pos = file_item.get('match_position') + if match_pos and len(match_pos) == 2: + text_range['startColumn'] = int(match_pos[0]) + text_range['endColumn'] = int(match_pos[1]) + locations.append({ + 'message': message, + 'filePath': file_item.get('file_path') or '.', 'textRange': text_range, - } - primary_location = location - else: - for ix, file in enumerate(mobsfscan_issue['files']): - text_range = { - 'startLine': file['match_lines'][0], - 'endLine': file['match_lines'][1], - } - location = { - 'message': issue_data['description'], - 'filePath': file['file_path'], - 'textRange': text_range, - } - - if 'match_string' in file: - location['message'] += ' [%s]' % file['match_string'] - - if ix == 0: - primary_location = location - else: - secondary_locations.append(location) + }) + return locations[0], locations[1:] + + +def build_issue(rule_id, issue_dict): + """Build a SonarQube generic-issue issue object.""" + primary, secondary = build_locations(issue_dict) issue = { - 'engineId': 'mobsfscan', - 'type': 'VULNERABILITY', - 'severity': sonarqube_severity_mapping[issue_data['severity']], - 'primaryLocation': primary_location, + 'ruleId': rule_id, + 'primaryLocation': primary, } - if secondary_locations: - issue['secondaryLocations'] = secondary_locations + if secondary: + issue['secondaryLocations'] = secondary + # Rough effort by severity + severity = (issue_dict.get('metadata') or {}).get('severity', '').upper() + issue['effortMinutes'] = { + 'ERROR': 60, + 'WARNING': 30, + 'INFO': 15, + }.get(severity, 30) return issue def sonarqube_output(outfile, scan_results, version): - """Sonarqube JSON Output.""" - sonarqube_issues = [] - for k, v in scan_results['results'].items(): - issue = get_sonarqube_issue(v) - issue['ruleId'] = k - sonarqube_issues.append(issue) - sonarqube_report = { - 'issues': sonarqube_issues, + """Return SonarQube generic issues JSON (rules + issues).""" + del version # kept for CLI signature compatibility + rules = [] + issues = [] + for rule_id, issue_dict in (scan_results.get('results') or {}).items(): + rules.append(build_rule(rule_id, issue_dict)) + issues.append(build_issue(rule_id, issue_dict)) + + report = { + 'rules': rules, + 'issues': issues, } - return json_output(outfile, sonarqube_report, version) + jout = json.dumps( + report, + sort_keys=True, + indent=2, + separators=(',', ': ')) + if outfile: + with open(outfile, 'w') as of: + of.write(jout) + else: + print(jout) + return jout diff --git a/mobsfscan/ios_plist.py b/mobsfscan/ios_plist.py new file mode 100644 index 0000000..1314efa --- /dev/null +++ b/mobsfscan/ios_plist.py @@ -0,0 +1,139 @@ +# -*- coding: utf_8 -*- +"""Analyze iOS App Transport Security settings in Info.plist.""" +from plistlib import load + +from mobsfscan.logger import init_logger +from mobsfscan.manifest import add_finding, mobsfscan_format +from mobsfscan.utils import report_path + + +logger = init_logger(__name__) +_WEAK_TLS = {'TLSv1.0', 'TLSv1.1'} +_TLS_12 = 'TLSv1.2' + + +def _enabled(value): + """Return whether a plist boolean-like value is enabled.""" + return value is True or str(value).upper() in {'TRUE', 'YES', '1'} + + +def _disabled(value): + """Return whether an explicitly configured value is disabled.""" + return value is False or str(value).upper() in {'FALSE', 'NO', '0'} + + +def scan_plists(plist_paths, validate_func): + """Scan Info.plist files for App Transport Security exceptions.""" + findings = [] + for plist_path in plist_paths: + try: + if not validate_func(plist_path): + continue + with plist_path.open('rb') as plist_file: + plist = load(plist_file) + except Exception: + logger.warning('Failed to parse plist: %s', plist_path) + continue + findings.extend( + check_transport_security( + report_path(plist_path), + plist, + ), + ) + return mobsfscan_format(findings) + + +def check_transport_security(plist_path, plist): + """Return findings for insecure ATS settings in one plist.""" + findings = [] + ats = plist.get('NSAppTransportSecurity') + if not isinstance(ats, dict): + return findings + + global_rules = { + 'NSAllowsArbitraryLoads': 'ios_ats_arbitrary_loads', + 'NSAllowsArbitraryLoadsForMedia': ( + 'ios_ats_arbitrary_loads_for_media' + ), + 'NSAllowsArbitraryLoadsInWebContent': ( + 'ios_ats_arbitrary_loads_in_web_content' + ), + 'NSAllowsLocalNetworking': 'ios_ats_local_networking', + } + for key, rule_id in global_rules.items(): + if _enabled(ats.get(key)): + add_finding(findings, plist_path, rule_id) + + domains = ats.get('NSExceptionDomains') or {} + if not isinstance(domains, dict): + return findings + for domain, config in domains.items(): + if not isinstance(config, dict): + continue + _check_exception_domain(findings, plist_path, str(domain), config) + return findings + + +def _check_exception_domain(findings, plist_path, domain, config): + """Check one NSExceptionDomains entry.""" + insecure_http_keys = ( + 'NSExceptionAllowsInsecureHTTPLoads', + 'NSTemporaryExceptionAllowsInsecureHTTPLoads', + 'NSThirdPartyExceptionAllowsInsecureHTTPLoads', + ) + if ( + domain not in {'localhost', '127.0.0.1'} + and any(_enabled(config.get(key)) for key in insecure_http_keys) + ): + add_finding( + findings, + plist_path, + 'ios_ats_insecure_http_loads', + (domain,), + ) + + minimum_tls = ( + config.get('NSExceptionMinimumTLSVersion') + or config.get('NSTemporaryExceptionMinimumTLSVersion') + ) + if minimum_tls in _WEAK_TLS: + add_finding( + findings, + plist_path, + 'ios_ats_weak_tls', + (minimum_tls, domain), + ) + elif minimum_tls == _TLS_12: + add_finding( + findings, + plist_path, + 'ios_ats_tls12', + (domain,), + ) + + forward_secrecy_keys = ( + 'NSExceptionRequiresForwardSecrecy', + 'NSTemporaryExceptionRequiresForwardSecrecy', + 'NSThirdPartyExceptionRequiresForwardSecrecy', + ) + if any( + key in config and _disabled(config[key]) + for key in forward_secrecy_keys + ): + add_finding( + findings, + plist_path, + 'ios_ats_forward_secrecy_disabled', + (domain,), + ) + + if ( + 'NSRequiresCertificateTransparency' in config + and _disabled(config['NSRequiresCertificateTransparency']) + ): + add_finding( + findings, + plist_path, + 'ios_ats_certificate_transparency_disabled', + (domain,), + ) diff --git a/mobsfscan/manifest.py b/mobsfscan/manifest.py index e5b540c..cd2db4c 100644 --- a/mobsfscan/manifest.py +++ b/mobsfscan/manifest.py @@ -1,5 +1,6 @@ # -*- coding: utf_8 -*- -"""Parse Android Manifest and NSC.""" +"""Parse Android manifest, network security config, and resource XML.""" +import re from operator import itemgetter from copy import deepcopy @@ -13,6 +14,7 @@ from mobsfscan.manifest_metadata import metadata from mobsfscan.utils import ( is_number, + report_path, valid_host, ) @@ -20,6 +22,10 @@ ANDROID_8_0_LEVEL = 26 ANDROID_9_0_LEVEL = 28 ANDROID_10_0_LEVEL = 29 +SENSITIVE_INPUT_NAME = re.compile( + r'(?:password|passcode|pin|secret|otp|token)', + re.IGNORECASE, +) ANDROID_API_LEVEL_MAP = { '1': '1.0', '2': '1.1', @@ -75,7 +81,7 @@ def scan_manifest(xml_paths, validate_func): logger.warning('Failed to parse XML: %s', xml_path) if p: findings = do_checks( - xml_path.resolve().as_posix(), p) + report_path(xml_path), p) if findings: results.extend(findings) return mobsfscan_format(results) @@ -114,7 +120,7 @@ def mobsfscan_format(results): def do_checks(xml_path, p): - """Run checks on android manifest and network security config.""" + """Run checks on supported Android XML documents.""" findings = [] if p.get('manifest') and p.get('manifest').get('application'): # Android Manifest @@ -146,9 +152,46 @@ def do_checks(xml_path, p): # Network Security Config nsc = NetworkSecurityChecks(findings, xml_path) nsc.network_security_checks(p) + else: + layout_sensitive_input_checks(findings, xml_path, p) return findings +def layout_sensitive_input_checks(findings, xml_path, document): + """Find sensitive EditText controls that permit keyboard suggestions.""" + def walk(node): + if isinstance(node, list): + for item in node: + yield from walk(item) + return + if not isinstance(node, dict): + return + for tag, child in node.items(): + if isinstance(child, (dict, list)): + yield tag, child + yield from walk(child) + + for tag, attrs in walk(document): + if not tag.lower().endswith('edittext') or not isinstance(attrs, dict): + continue + identity = ' '.join(str(attrs.get(name, '')) for name in ( + '@android:id', + '@android:hint', + '@android:contentDescription', + '@android:autofillHints', + )) + if not SENSITIVE_INPUT_NAME.search(identity): + continue + input_type = str(attrs.get('@android:inputType', '')).lower() + if 'password' in input_type or 'nosuggestions' in input_type: + continue + add_finding( + findings, + xml_path, + 'android_layout_sensitive_input_keyboard_cache', + ) + + def add_finding(findings, xml_file, rule_id, dynamic=None): """Append Findings.""" meta = deepcopy(metadata[rule_id]) @@ -249,7 +292,8 @@ def trust_cert_and_cert_pinning_bypass(self, cert, typ): def cert_instance_check(self, config, typ): """Check for cert instance.""" - certs = config.get('trust-anchors').get('certificates') + trust_anchors = config.get('trust-anchors') or {} + certs = trust_anchors.get('certificates') if isinstance(certs, dict): # Single cert instance self.trust_cert_and_cert_pinning_bypass( @@ -260,35 +304,43 @@ def cert_instance_check(self, config, typ): self.trust_cert_and_cert_pinning_bypass( cert, typ) + def _as_config_list(self, conf): + """xmltodict: one node -> dict, many siblings -> list.""" + if not conf: + return [] + if isinstance(conf, list): + return conf + return [conf] + + def _check_domain_config(self, domain_conf): + """Check one domain-config (and nested domain-config children).""" + if not isinstance(domain_conf, dict): + return + typ = 'domain' + self.clear_text_traffic_permitted(domain_conf, typ) + for nested in self._as_config_list(domain_conf.get('domain-config')): + self._check_domain_config(nested) + trust_anchors = domain_conf.get('trust-anchors') + if trust_anchors and trust_anchors.get('certificates'): + self.cert_instance_check(domain_conf, typ) + def network_security_checks(self, parsed_xml): """Android Network Security Config checks.""" + nsc = parsed_xml.get('network-security-config') or {} # Base Config - if parsed_xml.get('network-security-config').get('base-config'): + if nsc.get('base-config'): typ = 'base' - base_conf = parsed_xml.get( - 'network-security-config').get('base-config') - # Clear text traffic - self.clear_text_traffic_permitted(base_conf, typ) - if (base_conf.get('trust-anchors') - and base_conf.get('trust-anchors').get('certificates')): - # Trust user certs - self.cert_instance_check(base_conf, typ) - - # Domain config - if parsed_xml.get('network-security-config').get('domain-config'): - typ = 'domain' - domain_conf = parsed_xml.get( - 'network-security-config').get('domain-config') - # Domain config clear text - self.clear_text_traffic_permitted(domain_conf, typ) - if domain_conf.get('domain-config'): - # Nested domain config clear text - self.clear_text_traffic_permitted( - domain_conf.get('domain-config'), typ) - if (domain_conf.get('trust-anchors') - and domain_conf.get('trust-anchors').get('certificates')): - # Trust user certs - self.cert_instance_check(domain_conf, typ) + base_conf = nsc.get('base-config') + if isinstance(base_conf, dict): + # Clear text traffic + self.clear_text_traffic_permitted(base_conf, typ) + trust_anchors = base_conf.get('trust-anchors') + if trust_anchors and trust_anchors.get('certificates'): + self.cert_instance_check(base_conf, typ) + + # Domain config (one or many sibling blocks — see #87) + for domain_conf in self._as_config_list(nsc.get('domain-config')): + self._check_domain_config(domain_conf) class AppLinksCheck: diff --git a/mobsfscan/manifest_metadata.py b/mobsfscan/manifest_metadata.py index a251373..262b7c4 100644 --- a/mobsfscan/manifest_metadata.py +++ b/mobsfscan/manifest_metadata.py @@ -319,4 +319,154 @@ 'Communication.md'), }, }, + 'android_layout_sensitive_input_keyboard_cache': { + 'message': ( + 'A sensitive input field is configured without a password ' + 'input type or textNoSuggestions. Disable suggestions for ' + 'sensitive input to reduce exposure through keyboard learning ' + 'and suggestion history.'), + 'severity': 'WARNING', + 'reference': 'sensitive EditText without protected inputType', + 'metadata': { + 'cwe': 'cwe-524', + 'owasp-mobile': 'm1', + 'masvs': 'storage-5', + 'reference': ( + 'https://mas.owasp.org/MASTG/tests/android/' + 'MASVS-STORAGE/MASTG-TEST-0005/'), + }, + }, + # iOS App Transport Security (Info.plist) + 'ios_ats_arbitrary_loads': { + 'message': ( + 'App Transport Security is disabled for all network ' + 'connections by NSAllowsArbitraryLoads.'), + 'severity': 'ERROR', + 'reference': 'NSAllowsArbitraryLoads=true', + 'metadata': { + 'cwe': 'cwe-319', + 'owasp-mobile': 'm5', + 'masvs': 'network-1', + 'reference': ( + 'https://developer.apple.com/documentation/bundleresources/' + 'information-property-list/nsapptransportsecurity'), + }, + }, + 'ios_ats_arbitrary_loads_for_media': { + 'message': ( + 'App Transport Security is disabled for media loaded through ' + 'AVFoundation.'), + 'severity': 'ERROR', + 'reference': 'NSAllowsArbitraryLoadsForMedia=true', + 'metadata': { + 'cwe': 'cwe-319', + 'owasp-mobile': 'm5', + 'masvs': 'network-1', + 'reference': ( + 'https://developer.apple.com/documentation/bundleresources/' + 'information-property-list/nsapptransportsecurity'), + }, + }, + 'ios_ats_arbitrary_loads_in_web_content': { + 'message': ( + 'App Transport Security is disabled for requests made from ' + 'WebViews.'), + 'severity': 'ERROR', + 'reference': 'NSAllowsArbitraryLoadsInWebContent=true', + 'metadata': { + 'cwe': 'cwe-319', + 'owasp-mobile': 'm5', + 'masvs': 'network-1', + 'reference': ( + 'https://developer.apple.com/documentation/bundleresources/' + 'information-property-list/nsapptransportsecurity'), + }, + }, + 'ios_ats_local_networking': { + 'message': ( + 'App Transport Security restrictions are disabled for local ' + 'network connections.'), + 'severity': 'INFO', + 'reference': 'NSAllowsLocalNetworking=true', + 'metadata': { + 'cwe': 'cwe-319', + 'owasp-mobile': 'm5', + 'masvs': 'network-1', + 'reference': ( + 'https://developer.apple.com/documentation/bundleresources/' + 'information-property-list/nsapptransportsecurity'), + }, + }, + 'ios_ats_insecure_http_loads': { + 'message': 'ATS permits insecure HTTP loads for domain {}.', + 'severity': 'ERROR', + 'reference': 'NSExceptionAllowsInsecureHTTPLoads=true', + 'metadata': { + 'cwe': 'cwe-319', + 'owasp-mobile': 'm5', + 'masvs': 'network-1', + 'reference': ( + 'https://developer.apple.com/documentation/bundleresources/' + 'information-property-list/nsapptransportsecurity/' + 'nsexceptiondomains'), + }, + }, + 'ios_ats_weak_tls': { + 'message': 'ATS minimum TLS version is {} for domain {}.', + 'severity': 'ERROR', + 'reference': 'NSExceptionMinimumTLSVersion=TLSv1.0/TLSv1.1', + 'metadata': { + 'cwe': 'cwe-326', + 'owasp-mobile': 'm5', + 'masvs': 'network-2', + 'reference': ( + 'https://developer.apple.com/documentation/bundleresources/' + 'information-property-list/nsapptransportsecurity/' + 'nsexceptiondomains'), + }, + }, + 'ios_ats_tls12': { + 'message': ( + 'ATS minimum TLS version is TLSv1.2 for domain {}; prefer ' + 'TLSv1.3 where supported.'), + 'severity': 'WARNING', + 'reference': 'NSExceptionMinimumTLSVersion=TLSv1.2', + 'metadata': { + 'cwe': 'cwe-326', + 'owasp-mobile': 'm5', + 'masvs': 'network-2', + 'reference': ( + 'https://developer.apple.com/documentation/bundleresources/' + 'information-property-list/nsapptransportsecurity/' + 'nsexceptiondomains'), + }, + }, + 'ios_ats_forward_secrecy_disabled': { + 'message': 'ATS forward secrecy is disabled for domain {}.', + 'severity': 'ERROR', + 'reference': 'NSExceptionRequiresForwardSecrecy=false', + 'metadata': { + 'cwe': 'cwe-326', + 'owasp-mobile': 'm5', + 'masvs': 'network-2', + 'reference': ( + 'https://developer.apple.com/documentation/bundleresources/' + 'information-property-list/nsapptransportsecurity/' + 'nsexceptiondomains'), + }, + }, + 'ios_ats_certificate_transparency_disabled': { + 'message': 'ATS certificate transparency is disabled for domain {}.', + 'severity': 'WARNING', + 'reference': 'NSRequiresCertificateTransparency=false', + 'metadata': { + 'cwe': 'cwe-295', + 'owasp-mobile': 'm5', + 'masvs': 'network-3', + 'reference': ( + 'https://developer.apple.com/documentation/bundleresources/' + 'information-property-list/nsapptransportsecurity/' + 'nsexceptiondomains'), + }, + }, } diff --git a/mobsfscan/mobsfscan.py b/mobsfscan/mobsfscan.py index 45d821d..5a0a255 100644 --- a/mobsfscan/mobsfscan.py +++ b/mobsfscan/mobsfscan.py @@ -11,6 +11,7 @@ from mobsfscan.logger import init_logger from mobsfscan import settings from mobsfscan import manifest +from mobsfscan import ios_plist from mobsfscan.utils import ( get_best_practices, get_config, @@ -40,6 +41,7 @@ def __init__( 'ignore_paths': self.conf['ignore_paths'], 'ignore_rules': self.conf['ignore_rules'], 'severity_filter': self.conf['severity_filter'], + 'severity_overrides': self.conf['severity_overrides'], 'show_progress': not json, 'multiprocessing': mp, } @@ -49,49 +51,65 @@ def __init__( 'errors': [], } self.xmls = [] - self.best_practices = None + self.plists = [] + # Extensions whose BP presence hits must be stripped (may be a + # superset of languages actually present — see missing_controls). + self.best_practices_invert = set() + # Extensions for which absent controls are reported as missing. + self.best_practices_missing = set() self.standards = standards.get_standards() self.get_extensions() self.get_xmls() + self.get_plists() - def rules_selector(self, suffix): - """Get rule extensions from suffix.""" - if self.scan_type == 'android': - suffix = '.kt' - elif self.scan_type == 'ios': - suffix = '.swift' - # Default to .kt/.swift best practices if scan_type is specified - if suffix in ['.java', '.kt']: - if suffix == '.java': - self.best_practices = '.java' - else: - self.best_practices = '.kt' - self.options['match_rules'] = settings.ANDROID_RULES_DIR.as_posix() - self.options['sgrep_rules'] = settings.SGREP_RULES_DIR.as_posix() - self.options['sgrep_extensions'] = {'.java'} - self.options['match_extensions'] = {'.kt'} - elif suffix in {'.swift', '.m'}: - if suffix == '.swift': - self.best_practices = '.swift' - else: - self.best_practices = '.m' - self.options['match_rules'] = settings.IOS_RULES_DIR.as_posix() - self.options['match_extensions'] = {'.m', '.swift'} + def _configure_android(self, present): + """Semgrep Android rules; invert both Java and Kotlin BP dialects.""" + self.options['match_rules'] = None + self.options['match_extensions'] = None + self.options['sgrep_rules'] = settings.SGREP_RULES_DIR.as_posix() + self.options['sgrep_extensions'] = {'.java', '.kt'} + # Presence rules for both dialects are loaded; always strip both. + self.best_practices_invert = {'.java', '.kt'} + # Only report missing controls for languages actually in the scan. + self.best_practices_missing = set(present) + + def _configure_ios(self, present): + """Swift Semgrep + ObjC regex; invert both iOS BP dialects.""" + self.options['match_rules'] = ( + settings.IOS_RULES_DIR / 'objectivec').as_posix() + self.options['match_extensions'] = {'.m'} + self.options['sgrep_rules'] = settings.SGREP_RULES_DIR.as_posix() + self.options['sgrep_extensions'] = {'.swift'} + self.best_practices_invert = {'.swift', '.m'} + self.best_practices_missing = set(present) def get_extensions(self) -> set: - """Get extensions to scan.""" + """Discover source suffixes and configure scanners.""" scan_suffix = {'.java', '.kt', '.swift', '.m'} + found = set() for path in self.paths: pobj = Path(path) if pobj.is_dir(): for pfile in pobj.rglob('*'): - if pfile.suffix not in scan_suffix: - continue - return self.rules_selector(pfile.suffix) - else: - if pobj.suffix not in scan_suffix: - continue - return self.rules_selector(pobj.suffix) + if pfile.suffix in scan_suffix: + found.add(pfile.suffix) + elif pobj.suffix in scan_suffix: + found.add(pobj.suffix) + + android = found & {'.java', '.kt'} + ios = found & {'.swift', '.m'} + # Configure only when matching sources exist (plist/xml-only trees + # should not invent missing-control findings for .kt/.swift). + if self.scan_type == 'android': + if android: + self._configure_android(android) + elif self.scan_type == 'ios': + if ios: + self._configure_ios(ios) + elif android: + self._configure_android(android) + elif ios: + self._configure_ios(ios) def get_xmls(self) -> set: """Get XML files for scanning.""" @@ -105,6 +123,16 @@ def get_xmls(self) -> set: if pobj.suffix == '.xml': self.xmls.append(pobj) + def get_plists(self) -> set: + """Get Info.plist files for scanning.""" + for path in self.paths: + pobj = Path(path) + if pobj.is_dir(): + for pfile in pobj.rglob('Info.plist'): + self.plists.append(pfile) + elif pobj.name == 'Info.plist': + self.plists.append(pobj) + def scan(self) -> dict: """Start Scan.""" scanner = Scanner(self.options, self.paths) @@ -119,6 +147,16 @@ def scan(self) -> dict: logger.warning( 'Android XML checks failed. ' 'Please report to mobsfscan project') + try: + if self.plists and self.scan_type in ('auto', 'ios'): + result['plist_checks'] = ios_plist.scan_plists( + self.plists, + scanner.validate_file, + ) + except Exception: + logger.warning( + 'iOS Info.plist checks failed. ' + 'Please report to mobsfscan project') if result: self.format_output(result) @@ -127,11 +165,12 @@ def scan(self) -> dict: def format_output(self, results) -> dict: """Format to mobsfscan friendly output.""" self.format_semgrep(results.get('semantic_grep')) - # TODO: When we support kotlin semgrep, this needs rework self.format_pattern(results.get('pattern_matcher')) self.format_pattern(results.get('xml_checks')) + self.format_pattern(results.get('plist_checks')) self.missing_controls() self.post_ignore_rules() + self.post_override_severities() self.post_ignore_rules_by_severity() self.post_ignore_files() self.deduplicate_files() @@ -180,25 +219,33 @@ def format_xml(self, res_out): self.result['results'].update(res_out) def missing_controls(self): - """Check for missing controls.""" - if not self.best_practices: + """Check for missing controls. + + Semgrep loads best-practice *presence* rules for every dialect under + SGREP_RULES_DIR. Invert (delete) the union of related dialects so + presence hits never leak, but only *report* missing controls for + languages actually present in the scan paths. + """ + if not self.best_practices_invert and not self.best_practices_missing: return - ids, rules = get_best_practices(self.best_practices) - result_keys = self.result['results'].keys() + invert_ids, _ = get_best_practices(self.best_practices_invert) + missing_ids, rules = get_best_practices(self.best_practices_missing) + result_keys = set(self.result['results'].keys()) deleted = set() - for rule_id in ids: + for rule_id in invert_ids: if rule_id in result_keys: # Control Present deleted.add(rule_id) del self.result['results'][rule_id] - # Add Missing - missing = ids.difference(result_keys) - for rule_id in missing: + # Add Missing (only for languages present in the scan) + for rule_id in missing_ids.difference(result_keys): if rule_id in deleted: continue + details = rules.get(rule_id) + if not details: + continue self.result['results'][rule_id] = {} res = self.result['results'][rule_id] - details = rules[rule_id] res['metadata'] = details['metadata'] res['metadata']['description'] = details['message'] res['metadata']['severity'] = details['severity'] @@ -221,6 +268,20 @@ def post_ignore_rules(self): if rule_id in self.result['results']: del self.result['results'][rule_id] + def post_override_severities(self): + """Override finding severities from .mobsf severity-overrides.""" + overrides = self.options.get('severity_overrides') or {} + if not overrides: + return + for rule_id, severity in overrides.items(): + details = self.result['results'].get(rule_id) + if not details: + continue + meta = details.get('metadata') + if not isinstance(meta, dict): + continue + meta['severity'] = severity + def post_ignore_rules_by_severity(self): """Filter findings by rule severity.""" del_keys = set() @@ -233,51 +294,49 @@ def post_ignore_rules_by_severity(self): del self.result['results'][rid] def suppress_pm_comments(self, obj, rule_id): - """Suppress pattern matcher.""" - file_path = obj['file_path'] - lines = obj['match_lines'] - if lines[0] != lines[1]: - # Skip multiline for now + """Return True if this match has a mobsf-ignore for rule_id.""" + file_path = obj.get('file_path') + lines = obj.get('match_lines') or (0, 0) + start, end = int(lines[0]), int(lines[1]) + if start <= 0: return False - match_line = getline(file_path, lines[0]) - if 'mobsf-ignore:' in match_line and rule_id in match_line: - return True + if end < start: + end = start + # Check every line in the reported span (covers libsast + # off-by-one when a match starts at column 0). + for lineno in range(start, end + 1): + match_line = getline(file_path, lineno) + if self._line_ignores_rule(match_line, rule_id): + return True return False - def remove_matches(self, file, files): - """Remove all matches in the file for the rule.""" - new_files = [] - lines = [] - for af in files: - # Collect all match lines for the rule in the file - if file['file_path'] == af['file_path']: - lines.append(af['match_lines']) - # Add all files except the file with matching lines - for af in files: - if af['match_lines'] not in lines: - new_files.append(af) - elif af['file_path'] != file['file_path']: - new_files.append(af) - return new_files + @staticmethod + def _line_ignores_rule(match_line, rule_id): + """Parse // mobsf-ignore: id1, id2 on a source line.""" + if not match_line or 'mobsf-ignore:' not in match_line: + return False + marker = match_line.split('mobsf-ignore:', 1)[1] + # Stop at end of line comment content; split rule ids + ids = [] + for part in marker.replace(',', ' ').split(): + token = part.strip().strip(',') + if token: + ids.append(token) + return rule_id in ids def post_ignore_files(self): - """Ignore file by rule.""" + """Drop individual matches suppressed by mobsf-ignore comments.""" del_keys = set() for rule_id, details in self.result['results'].items(): files = details.get('files') if not files: continue - tmp_files = files - for file in files: - # check if ignore comment is present for - # any matches in the file for the rule - if self.suppress_pm_comments(file, rule_id): - # remove all matches of the file for the rule - tmp_files = self.remove_matches(file, files) - if len(tmp_files) == 0: - del_keys.add(rule_id) - details['files'] = tmp_files - # Remove Rule IDs marked for deletion. + kept = [ + match for match in files + if not self.suppress_pm_comments(match, rule_id) + ] + details['files'] = kept + if not kept: + del_keys.add(rule_id) for rid in del_keys: - if rid in self.result['results']: - del self.result['results'][rid] + self.result['results'].pop(rid, None) diff --git a/mobsfscan/rules/patterns/android/kotlin/best_practices.yaml b/mobsfscan/rules/patterns/android/kotlin/best_practices.yaml deleted file mode 100644 index e2ff7a8..0000000 --- a/mobsfscan/rules/patterns/android/kotlin/best_practices.yaml +++ /dev/null @@ -1,103 +0,0 @@ -# All best practices rules are evaluated differently. -# Unlike other rules which are triggered when we find those patterns in code, -# these are triggered only when we do not find a pattern after scanning the entire codebase. -- id: android_safetynet - message: >- - This app does not uses SafetyNet Attestation API that provides - cryptographically-signed attestation, assessing the device's integrity. - This check helps to ensure that the servers are interacting with the - genuine app running on a genuine Android device. - type: Regex - pattern: com.google\.android\.gms\.safetynet\.SafetyNetApi - severity: INFO - input_case: exact - metadata: - cwe: cwe-353 - owasp-mobile: m8 - masvs: resilience-1 - reference: >- - https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05j-Testing-Resiliency-Against-Reverse-Engineering.md#testing-root-detection-mstg-resilience-1 -- id: android_prevent_screenshot - message: >- - This app does not have capabilities to prevent against Screenshots from Recent Task - History/ Now On Tap etc. - type: RegexAndOr - pattern: - - \.FLAG_SECURE - - - setFlags\( - - addFlags\( - severity: INFO - input_case: exact - metadata: - cwe: cwe-200 - owasp-mobile: m2 - masvs: storage-9 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#finding-sensitive-information-in-auto-generated-screenshots-mstg-storage-9 -- id: android_root_detection - message: >- - This app does not have root detection capabilities. Running a sensitive - application on a rooted device questions the device integrity and affects - users data. - type: RegexOr - pattern: - - \.isRooted - - \.isDeviceRooted\( - - \.isJailBroken\( - - RootTools\.isAccessGiven\( - - \.contains\(\"test-keys\"\) - severity: INFO - input_case: exact - metadata: - cwe: cwe-919 - owasp-mobile: m8 - masvs: resilience-1 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05j-Testing-Resiliency-Against-Reverse-Engineering.md#testing-root-detection-mstg-resilience-1 -- id: android_tapjacking - message: This app does not have capabilities to prevent tapjacking attacks. - type: Regex - pattern: setFilterTouchesWhenObscured\(true\) - severity: INFO - input_case: exact - metadata: - cwe: cwe-200 - owasp-mobile: m1 - masvs: platform-9 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05h-Testing-Platform-Interaction.md#testing-for-overlay-attacks-mstg-platform-9 -- id: android_certificate_transparency - message: >- - This app does not enforce TLS Certificate Transparency which helps to - detect SSL certificates that have been mistakenly issued by a - certificate authority or maliciously acquired from an otherwise - unimpeachable certificate authority. - type: RegexOr - pattern: - - CTHostnameVerifierBuilder\( - - CTInterceptorBuilder\( - severity: INFO - input_case: exact - metadata: - cwe: cwe-295 - owasp-mobile: m3 - masvs: network-4 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05g-Testing-Network-Communication.md#testing-custom-certificate-stores-and-certificate-pinning-mstg-network-4 -- id: android_ssl_pinning - message: >- - This app does not use TLS/SSL certificate or public key pinning to detect - or prevent MITM attacks in secure communication channel. - type: RegexAndOr - pattern: - - org\.thoughtcrime\.ssl\.pinning|\.getTrustManagers\(|TrustManagerFactory\.|CertificatePinner\.Builder\(|Retrofit\.Builder\(|Picasso\.Builder\(|\.setHostnameVerifier\( - - - PinningHelper\.getPinnedHttpsURLConnection|PinningHelper\.getPinnedHttpClient|PinningSSLSocketFactory\( - - \.setCertificateEntry\(|trustedChain|\.init\( - - \.add\( - - \.baseUrl\( - - \.downloader\( - - PinningHostnameVerifier\(|\.verify\(|DynamicPinningHostnameVerifier\( - severity: INFO - input_case: exact - metadata: - cwe: cwe-295 - owasp-mobile: m3 - masvs: network-4 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05g-Testing-Network-Communication.md#testing-custom-certificate-stores-and-certificate-pinning-mstg-network-4 - diff --git a/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml b/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml deleted file mode 100644 index 2e46f27..0000000 --- a/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml +++ /dev/null @@ -1,344 +0,0 @@ -- id: android_kotlin_hiddenui - message: >- - Hidden elements in view can be used to hide data from user. But this data - can be leaked. - type: Regex - pattern: View\.GONE|View\.INVISIBLE - severity: ERROR - input_case: exact - metadata: - cwe: cwe-919 - owasp-mobile: m1 - masvs: storage-7 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#checking-for-sensitive-data-disclosure-through-the-user-interface-mstg-storage-7 -- id: android_kotlin_insecure_ssl - message: >- - Insecure Implementation of SSL. Trusting all the certificates or accepting - self signed certificates is a critical Security Hole. This application is - vulnerable to MITM attacks - type: RegexAnd - pattern: - - javax\.net\.ssl - - >- - TrustAllSSLSocket-Factory|AllTrustSSLSocketFactory|NonValidatingSSLSocketFactory|net\.SSLCertificateSocketFactory|ALLOW_ALL_HOSTNAME_VERIFIER|\.setDefaultHostnameVerifier\(|NullHostnameVerifier\( - severity: ERROR - input_case: exact - metadata: - cwe: cwe-295 - owasp-mobile: m3 - masvs: network-3 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05g-Testing-Network-Communication.md#android-network-apis -- id: android_kotlin_webview_external - message: >- - WebView load files from external storage. Files in external storage can be - modified by any application. - type: RegexAnd - pattern: - - \.loadUrl\(.{0,48}getExternalStorageDirectory\( - - webkit\.WebView - severity: ERROR - input_case: exact - metadata: - cwe: cwe-749 - owasp-mobile: m1 - masvs: platform-6 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05h-Testing-Platform-Interaction.md#static-analysis-7 -- id: android_kotlin_insecure_random - message: The App uses an insecure Random Number Generator. - type: Regex - pattern: java\.util\.Random(?!Access) - severity: WARNING - metadata: - input_case: exact - cwe: cwe-330 - owasp-mobile: m5 - masvs: crypto-6 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#weak-random-number-generators -- id: android_kotlin_logging - message: The App logs information. Sensitive information should never be logged. - type: Regex - pattern: Log\.(v|d|i|w|e|f|s)|System\.out\.print|System\.err\.print - severity: INFO - metadata: - input_case: exact - cwe: cwe-532 - owasp-mobile: m1 - masvs: storage-3 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#logs -- id: android_kotlin_webview - message: >- - Insecure WebView Implementation. Execution of user controlled code in - WebView is a critical Security Hole. - type: RegexAnd - pattern: - - setJavaScriptEnabled\(true\) - - \.addJavascriptInterface\( - severity: WARNING - input_case: exact - metadata: - cwe: cwe-749 - owasp-mobile: m1 - masvs: platform-7 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05h-Testing-Platform-Interaction.md#testing-javascript-execution-in-webviews-mstg-platform-5 -- id: android_kotlin_webview_allow_file_from_url - message: >- - Ensure that user controlled URLs never reaches the Webview. Enabling file access - from URLs in WebView can leak sensitive information from the file system. - type: RegexAndOr - pattern: - - setJavaScriptEnabled\(true\) - - - \.setAllowFileAccessFromFileURLs\(true\) - - \.setAllowUniversalAccessFromFileURLs\(true\) - severity: WARNING - input_case: exact - metadata: - cvss: 6.1 - cwe: cwe-200 - owasp-mobile: m1 - masvs: platform-7 - ref: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05h-Testing-Platform-Interaction.md#static-analysis-6 -- id: android_kotlin_webview_debug - message: Remote WebView debugging is enabled. - type: RegexAnd - pattern: - - \.setWebContentsDebuggingEnabled\(true\) - - WebView - severity: ERROR - input_case: exact - metadata: - cwe: cwe-489 - owasp-mobile: m1 - masvs: resilience-2 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05j-Testing-Resiliency-Against-Reverse-Engineering.md#testing-anti-debugging-detection-mstg-resilience-2 -- id: android_kotlin_webview_ignore_ssl - message: >- - Insecure WebView Implementation. WebView ignores SSL Certificate errors and - accept any SSL Certificate. This application is vulnerable to MITM attacks - type: RegexAnd - pattern: - - onReceivedSslError\(WebView - - \.proceed\(\); - severity: ERROR - input_case: exact - metadata: - cwe: cwe-295 - owasp-mobile: m3 - masvs: network-3 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05g-Testing-Network-Communication.md#webview-server-certificate-verification -- id: android_kotlin_sql_raw_query - message: >- - App uses SQLite Database and execute raw SQL query. Untrusted user input in - raw SQL queries can cause SQL Injection. Also sensitive information should - be encrypted and written to the database. - type: RegexAndOr - pattern: - - android\.database\.sqlite - - - rawQuery\( - - execSQL\( - severity: WARNING - input_case: exact - metadata: - cwe: cwe-78 - owasp-mobile: m7 - masvs: platform-2 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04h-Testing-Code-Quality.md#injection-flaws-mstg-arch-2-and-mstg-platform-2 -- id: android_kotlin_jackson_deserialize - message: >- - The app uses jackson deserialization library. Deserialization of untrusted - input can result in arbitrary code execution. - type: RegexAnd - pattern: - - com\.fasterxml\.jackson\.databind\.ObjectMapper - - \.enableDefaultTyping\( - severity: ERROR - input_case: exact - metadata: - cwe: cwe-502 - owasp-mobile: m7 - masvs: platform-8 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05h-Testing-Platform-Interaction.md#testing-object-persistence-mstg-platform-8 -- id: android_kotlin_aes_ecb - message: >- - The App uses ECB mode in Cryptographic encryption algorithm. ECB mode is - known to be weak as it results in the same ciphertext for identical blocks - of plaintext. - type: Regex - pattern: Cipher\.getInstance\(\s*"\s*AES\/ECB - severity: ERROR - input_case: exact - metadata: - cwe: cwe-327 - owasp-mobile: m5 - masvs: crypto-2 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#weak-block-cipher-mode -- id: android_kotlin_aes_ecb_default - message: >- - Calling Cipher.getInstance("AES") will return AES ECB mode by default. ECB mode is - known to be weak as it results in the same ciphertext for identical blocks - of plaintext. - type: Regex - pattern: Cipher\.getInstance\("AES"\) - severity: ERROR - input_case: exact - metadata: - cwe: cwe-327 - owasp-mobile: m5 - masvs: crypto-2 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#weak-block-cipher-mode -- id: cbc_kotlin_padding_oracle - message: The App uses the encryption mode CBC with PKCS5/PKCS7 padding. This configuration is vulnerable to padding oracle attacks. - pattern: - - \.getInstance\(.{0,48}\/CBC\/PKCS5Padding - - \.getInstance\(.{0,48}\/CBC\/PKCS7Padding - type: RegexOr - severity: ERROR - input_case: exact - metadata: - masvs: crypto-3 - owasp-mobile: m5 - cwe: cwe-649 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 -- id: android_kotlin_rsa_no_oaep - message: >- - This App uses RSA Crypto without OAEP padding. The purpose of the padding - scheme is to prevent a number of attacks on RSA that only work when the - encryption is performed without padding. - type: Regex - pattern: cipher\.getinstance\(\"rsa/.{1,48}/nopadding - severity: ERROR - input_case: lower - metadata: - cwe: cwe-780 - owasp-mobile: m5 - masvs: crypto-3 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#mobile-app-cryptography -- id: android_kotlin_world_writable - message: The file is World Writable. Any App can write to the file - type: RegexOr - pattern: - - MODE_WORLD_WRITABLE - - 'openFileOutput\(\s*".{1,48}"\s*,\s*2\s*\)' - severity: WARNING - input_case: exact - metadata: - cwe: cwe-276 - owasp-mobile: m2 - masvs: storage-2 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#testing-local-storage-for-sensitive-data-mstg-storage-1-and-mstg-storage-2 -- id: android_kotlin_world_readable - message: The file is World Readable. Any App can read from the file - type: RegexOr - pattern: - - MODE_WORLD_READABLE - - 'openFileOutput\(\s*".{1,48}"\s*,\s*1\s*\)' - severity: WARNING - input_case: exact - metadata: - cwe: cwe-276 - owasp-mobile: m2 - masvs: storage-2 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#testing-local-storage-for-sensitive-data-mstg-storage-1-and-mstg-storage-2 -- id: android_kotlin_world_read_write - message: The file is World Readable and Writable. Any App can read/write to the file - type: Regex - pattern: 'openFileOutput\(\s*".{1,48}"\s*,\s*3\s*\)' - severity: WARNING - input_case: exact - metadata: - cwe: cwe-276 - owasp-mobile: m2 - masvs: storage-2 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#testing-local-storage-for-sensitive-data-mstg-storage-1-and-mstg-storage-2 -- id: android_kotlin_weak_hash - message: Weak Hash algorithm used. The hash algorithm is known to have hash collisions. - pattern: - - \.getInstance\(.{0,48}md4 - - \.getInstance\(.{0,48}MD4 - type: RegexOr - input_case: exact - severity: WARNING - metadata: - masvs: crypto-4 - owasp-mobile: m5 - cwe: cwe-327 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 -- id: android_kotlin_weak_ciphers - message: Weak Encryption algorithm used - severity: ERROR - type: RegexOr - input_case: exact - pattern: - - \.getInstance\(.{0,48}rc2 - - \.getInstance\(.{0,48}RC2 - - \.getInstance\(.{0,48}rc4 - - \.getInstance\(.{0,48}RC4 - - \.getInstance\(.{0,48}blowfish - - \.getInstance\(.{0,48}BLOWFISH - - Cipher\.getInstance\(.{0,48}DES - - Cipher\.getInstance\(.{0,48}des - metadata: - cwe: cwe-327 - masvs: crypto-4 - owasp-mobile: m5 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 -- id: android_kotlin_md5 - message: MD5 is a weak hash known to have hash collisions. - type: RegexOr - pattern: - - \.getInstance\(.{0,48}MD5 - - \.getInstance\(.{0,48}md5 - - DigestUtils\.md5\( - input_case: exact - severity: WARNING - metadata: - cwe: cwe-327 - masvs: crypto-4 - owasp-mobile: m5 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 -- id: android_kotlin_sha1 - message: SHA-1 is a weak hash known to have hash collisions. - type: RegexOr - input_case: exact - severity: WARNING - pattern: - - \.getInstance\(.{0,48}SHA-1 - - \.getInstance\(.{0,48}sha-1 - - \.getInstance\(.{0,48}SHA1 - - \.getInstance\(.{0,48}sha1 - - DigestUtils\.sha\( - metadata: - cwe: cwe-327 - masvs: crypto-4 - owasp-mobile: m5 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 -- id: android_kotlin_weak_iv - message: >- - The App may use weak IVs like "0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00" or - "0x01,0x02,0x03,0x04,0x05,0x06,0x07". Not using a random IV makes the - resulting ciphertext much more predictable and susceptible to a dictionary - attack. - input_case: exact - pattern: - - '0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00' - - '0x01,0x02,0x03,0x04,0x05,0x06,0x07' - severity: WARNING - type: RegexOr - metadata: - cwe: cwe-1204 - masvs: crypto-5 - owasp-mobile: m5 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#mobile-app-cryptography -- id: android_kotlin_hardcoded - message: >- - Files may contain hardcoded sensitive information like usernames, - passwords, keys etc. - input_case: lower - pattern: >- - (password\s*=\s*[\'|\"].{1,100}[\'|\"]\s{0,5})|(pass\s*=\s*[\'|\"].{1,100}[\'|\"]\s{0,5})|(username\s*=\s*[\'|\"].{1,100}[\'|\"]\s{0,5})|(secret\s*=\s*[\'|\"].{1,100}[\'|\"]\s{0,5})|(key\s*=\s*[\'|\"].{1,100}[\'|\"]\s{0,5}) - severity: WARNING - type: Regex - metadata: - masvs: storage-14 - cwe: cwe-798 - owasp-mobile: m9 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#storing-a-key---example diff --git a/mobsfscan/rules/patterns/ios/objectivec/objective_c_rules.yaml b/mobsfscan/rules/patterns/ios/objectivec/objective_c_rules.yaml index 326545b..dc1df55 100644 --- a/mobsfscan/rules/patterns/ios/objectivec/objective_c_rules.yaml +++ b/mobsfscan/rules/patterns/ios/objectivec/objective_c_rules.yaml @@ -2,7 +2,9 @@ message: Files may contain hardcoded sensitive information like usernames, passwords, keys etc. input_case: lower - pattern: (password\s*=\s*[\'|\"].{1,100}[\'|\"]\s{0,5})|(pass\s*=\s*[\'|\"].{1,100}[\'|\"]\s{0,5})|(username\s*=\s*[\'|\"].{1,100}[\'|\"]\s{0,5})|(secret\s*=\s*[\'|\"].{1,100}[\'|\"]\s{0,5})|(key\s*=\s*[\'|\"].{1,100}[\'|\"]\s{0,5}) + # Avoid matching UserDefaults/lookup names ending in Key (e.g. languageKey); see #111. + # Allow long literals (hex keys/PEMs); .{1,100} caused false negatives (#88). + pattern: (password\s*=\s*[\'|\"].{1,8192}[\'|\"]\s{0,5})|(pass\s*=\s*[\'|\"].{1,8192}[\'|\"]\s{0,5})|(username\s*=\s*[\'|\"].{1,8192}[\'|\"]\s{0,5})|(secret\s*=\s*[\'|\"].{1,8192}[\'|\"]\s{0,5})|((?- - Use of deprecated property tlsMinimumSupportedProtocol. To avoid potential - security risks, use tlsMinimumSupportedProtocolVersion - input_case: exact - pattern: \.tlsMinimumSupportedProtocol - severity: WARNING - type: Regex - metadata: - cwe: cwe-757 - masvs: network-2 - owasp-mobile: m3 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04f-Testing-Network-Communication.md#verifying-data-encryption-on-the-network-mstg-network-1-and-mstg-network-2 -- id: ios_uiwebview - message: >- - This app uses UIWebView. For security reasons, It is recommended to use WKWebView instead. - pattern: UIWebView - severity: INFO - type: Regex - input_case: exact - metadata: - cwe: cwe-919 - masvs: platform-5 - owasp-mobile: m1 - reference: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06h-Testing-Platform-Interaction.md#testing-ios-webviews-mstg-platform-5 diff --git a/mobsfscan/rules/semgrep/best_practices/android_safetynetapi.yaml b/mobsfscan/rules/semgrep/best_practices/java/android_safetynetapi.yaml similarity index 100% rename from mobsfscan/rules/semgrep/best_practices/android_safetynetapi.yaml rename to mobsfscan/rules/semgrep/best_practices/java/android_safetynetapi.yaml diff --git a/mobsfscan/rules/semgrep/best_practices/flag_secure.yaml b/mobsfscan/rules/semgrep/best_practices/java/flag_secure.yaml similarity index 100% rename from mobsfscan/rules/semgrep/best_practices/flag_secure.yaml rename to mobsfscan/rules/semgrep/best_practices/java/flag_secure.yaml diff --git a/mobsfscan/rules/semgrep/best_practices/root_detection.yaml b/mobsfscan/rules/semgrep/best_practices/java/root_detection.yaml similarity index 100% rename from mobsfscan/rules/semgrep/best_practices/root_detection.yaml rename to mobsfscan/rules/semgrep/best_practices/java/root_detection.yaml diff --git a/mobsfscan/rules/semgrep/best_practices/tapjacking.yaml b/mobsfscan/rules/semgrep/best_practices/java/tapjacking.yaml similarity index 100% rename from mobsfscan/rules/semgrep/best_practices/tapjacking.yaml rename to mobsfscan/rules/semgrep/best_practices/java/tapjacking.yaml diff --git a/mobsfscan/rules/semgrep/best_practices/tls_certificate_transparency.yaml b/mobsfscan/rules/semgrep/best_practices/java/tls_certificate_transparency.yaml similarity index 100% rename from mobsfscan/rules/semgrep/best_practices/tls_certificate_transparency.yaml rename to mobsfscan/rules/semgrep/best_practices/java/tls_certificate_transparency.yaml diff --git a/mobsfscan/rules/semgrep/best_practices/tls_pinning.yaml b/mobsfscan/rules/semgrep/best_practices/java/tls_pinning.yaml similarity index 100% rename from mobsfscan/rules/semgrep/best_practices/tls_pinning.yaml rename to mobsfscan/rules/semgrep/best_practices/java/tls_pinning.yaml diff --git a/mobsfscan/rules/semgrep/best_practices/kotlin/android_safetynet.yaml b/mobsfscan/rules/semgrep/best_practices/kotlin/android_safetynet.yaml new file mode 100644 index 0000000..911259a --- /dev/null +++ b/mobsfscan/rules/semgrep/best_practices/kotlin/android_safetynet.yaml @@ -0,0 +1,25 @@ +rules: + - id: android_safetynet + patterns: + - pattern-either: + - pattern: | + SafetyNet.getClient(...) + - pattern: | + val $C = SafetyNet.getClient(...) + ... + $C.attest(...) + - pattern-regex: 'com\.google\.android\.gms\.safetynet\.SafetyNetApi' + message: >- + This app does not uses SafetyNet Attestation API that provides + cryptographically-signed attestation, assessing the device's integrity. + This check helps to ensure that the servers are interacting with the + genuine app running on a genuine Android device. + languages: + - kotlin + severity: INFO + metadata: + cwe: cwe-353 + owasp-mobile: m8 + masvs: resilience-1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05j-Testing-Resiliency-Against-Reverse-Engineering.md#testing-root-detection-mstg-resilience-1 diff --git a/mobsfscan/rules/semgrep/best_practices/kotlin/flag_secure.yaml b/mobsfscan/rules/semgrep/best_practices/kotlin/flag_secure.yaml new file mode 100644 index 0000000..9bc1dca --- /dev/null +++ b/mobsfscan/rules/semgrep/best_practices/kotlin/flag_secure.yaml @@ -0,0 +1,30 @@ +rules: + - id: android_prevent_screenshot + patterns: + - pattern-either: + - pattern: | + $W.setFlags(WindowManager.LayoutParams.FLAG_SECURE, ...) + - pattern: | + $W.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + - pattern: | + $W.addFlags(WindowManager.LayoutParams.FLAG_SECURE, ...) + - pattern: | + val $V = WindowManager.LayoutParams.FLAG_SECURE + ... + $W.setFlags($V, ...) + - pattern: | + val $V = WindowManager.LayoutParams.FLAG_SECURE + ... + $W.addFlags($V) + message: >- + This app does not have capabilities to prevent against Screenshots from Recent Task + History/ Now On Tap etc. + languages: + - kotlin + severity: INFO + metadata: + cwe: cwe-200 + owasp-mobile: m2 + masvs: storage-9 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#finding-sensitive-information-in-auto-generated-screenshots-mstg-storage-9 diff --git a/mobsfscan/rules/semgrep/best_practices/kotlin/root_detection.yaml b/mobsfscan/rules/semgrep/best_practices/kotlin/root_detection.yaml new file mode 100644 index 0000000..e78a9ec --- /dev/null +++ b/mobsfscan/rules/semgrep/best_practices/kotlin/root_detection.yaml @@ -0,0 +1,27 @@ +rules: + - id: android_root_detection + patterns: + - pattern-either: + - pattern: | + $R.isRooted(...) + - pattern: | + $R.isDeviceRooted(...) + - pattern: | + $R.isJailBroken(...) + - pattern: | + RootTools.isAccessGiven(...) + - pattern: | + $S.contains("test-keys") + message: >- + This app does not have root detection capabilities. Running a sensitive + application on a rooted device questions the device integrity and affects + users data. + languages: + - kotlin + severity: INFO + metadata: + cwe: cwe-919 + owasp-mobile: m8 + masvs: resilience-1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05j-Testing-Resiliency-Against-Reverse-Engineering.md#testing-root-detection-mstg-resilience-1 diff --git a/mobsfscan/rules/semgrep/best_practices/kotlin/tapjacking.yaml b/mobsfscan/rules/semgrep/best_practices/kotlin/tapjacking.yaml new file mode 100644 index 0000000..5156215 --- /dev/null +++ b/mobsfscan/rules/semgrep/best_practices/kotlin/tapjacking.yaml @@ -0,0 +1,15 @@ +rules: + - id: android_tapjacking + patterns: + - pattern: | + $V.setFilterTouchesWhenObscured(true) + message: This app does not have capabilities to prevent tapjacking attacks. + languages: + - kotlin + severity: INFO + metadata: + cwe: cwe-200 + owasp-mobile: m1 + masvs: platform-9 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05h-Testing-Platform-Interaction.md#testing-for-overlay-attacks-mstg-platform-9 diff --git a/mobsfscan/rules/semgrep/best_practices/kotlin/tls_certificate_transparency.yaml b/mobsfscan/rules/semgrep/best_practices/kotlin/tls_certificate_transparency.yaml new file mode 100644 index 0000000..cae4571 --- /dev/null +++ b/mobsfscan/rules/semgrep/best_practices/kotlin/tls_certificate_transparency.yaml @@ -0,0 +1,22 @@ +rules: + - id: android_certificate_transparency + patterns: + - pattern-either: + - pattern: | + CTHostnameVerifierBuilder(...) + - pattern: | + CTInterceptorBuilder(...) + message: >- + This app does not enforce TLS Certificate Transparency which helps to + detect SSL certificates that have been mistakenly issued by a + certificate authority or maliciously acquired from an otherwise + unimpeachable certificate authority. + languages: + - kotlin + severity: INFO + metadata: + cwe: cwe-295 + owasp-mobile: m3 + masvs: network-4 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05g-Testing-Network-Communication.md#testing-custom-certificate-stores-and-certificate-pinning-mstg-network-4 diff --git a/mobsfscan/rules/semgrep/best_practices/kotlin/tls_pinning.yaml b/mobsfscan/rules/semgrep/best_practices/kotlin/tls_pinning.yaml new file mode 100644 index 0000000..703f0a2 --- /dev/null +++ b/mobsfscan/rules/semgrep/best_practices/kotlin/tls_pinning.yaml @@ -0,0 +1,37 @@ +rules: + - id: android_ssl_pinning + patterns: + - pattern-either: + - pattern: | + CertificatePinner.Builder() + - pattern: | + CertificatePinner.Builder(...) + - pattern: | + PinningHelper.getPinnedHttpsURLConnection(...) + - pattern: | + PinningHelper.getPinnedHttpClient(...) + - pattern: | + PinningSSLSocketFactory(...) + - pattern: | + PinningHostnameVerifier(...) + - pattern: | + DynamicPinningHostnameVerifier(...) + - pattern: | + TrustManagerFactory.getInstance(...) + - pattern: | + $X.setHostnameVerifier(...) + - pattern: | + $KS.setCertificateEntry(...) + - pattern-regex: 'org\.thoughtcrime\.ssl\.pinning' + message: >- + This app does not use TLS/SSL certificate or public key pinning to detect + or prevent MITM attacks in secure communication channel. + languages: + - kotlin + severity: INFO + metadata: + cwe: cwe-295 + owasp-mobile: m3 + masvs: network-4 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05g-Testing-Network-Communication.md#testing-custom-certificate-stores-and-certificate-pinning-mstg-network-4 diff --git a/mobsfscan/rules/semgrep/best_practices/swift/jailbreak.yaml b/mobsfscan/rules/semgrep/best_practices/swift/jailbreak.yaml new file mode 100644 index 0000000..ce64ea5 --- /dev/null +++ b/mobsfscan/rules/semgrep/best_practices/swift/jailbreak.yaml @@ -0,0 +1,14 @@ +rules: + - id: ios_jailbreak_detect + patterns: + - pattern-regex: '/Applications/Cydia\.app|/Library/MobileSubstrate/MobileSubstrate\.dylib|/usr/sbin/sshd|/etc/apt|cydia://|/var/lib/cydia|/Applications/FakeCarrier\.app|/Applications/Icy\.app|/Applications/IntelliScreen\.app|/Applications/SBSettings\.app|/Library/MobileSubstrate/DynamicLibraries/LiveClock\.plist|/System/Library/LaunchDaemons/com\.ikey\.bbot\.plist|/System/Library/LaunchDaemons/com\.saurik\.Cydia\.Startup\.plist|/etc/ssh/sshd_config|/private/var/tmp/cydia\.log|/usr/libexec/ssh-keysign|/Applications/MxTube\.app|/Applications/RockApp\.app|/Applications/WinterBoard\.app|/Applications/blackra1n\.app|/Library/MobileSubstrate/DynamicLibraries/Veency\.plist|/private/var/lib/apt|/private/var/lib/cydia|/private/var/mobile/Library/SBSettings/Themes|/private/var/stash|/usr/bin/sshd|/usr/libexec/sftp-server|/var/cache/apt|/var/lib/apt|/usr/sbin/frida-server|/usr/bin/cycript|/usr/local/bin/cycript|/usr/lib/libcycript\.dylib|frida-server|/etc/apt/sources\.list\.d/electra\.list|/etc/apt/sources\.list\.d/sileo\.sources|/\.bootstrapped_electra|/usr/lib/libjailbreak\.dylib|/jb/lzma|/\.cydia_no_stash|/\.installed_unc0ver|/jb/offsets\.plist|/usr/share/jailbreak/injectme\.plist|/usr/libexec/cydia/firmware\.sh|/private/var/cache/apt/|/Library/MobileSubstrate/CydiaSubstrate\.dylib' + message: This app does not have Jailbreak detection capabilities. + languages: + - swift + severity: INFO + metadata: + cwe: cwe-919 + masvs: resilience-1 + owasp-mobile: m8 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06j-Testing-Resiliency-Against-Reverse-Engineering.md#jailbreak-detection-mstg-resilience-1 diff --git a/mobsfscan/rules/semgrep/best_practices/swift/keyboard.yaml b/mobsfscan/rules/semgrep/best_practices/swift/keyboard.yaml new file mode 100644 index 0000000..b636994 --- /dev/null +++ b/mobsfscan/rules/semgrep/best_practices/swift/keyboard.yaml @@ -0,0 +1,34 @@ +rules: + - id: ios_custom_keyboard_disabled + patterns: + - pattern-regex: 'extensionPointIdentifier\s*==.{0,100}\.keyboard' + message: This app does not have custom keyboards disabled. + languages: + - swift + severity: INFO + metadata: + cwe: cwe-919 + masvs: platform-11 + owasp-mobile: m1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06h-Testing-Platform-Interaction.md#app-extensions + - id: ios_keyboard_cache + patterns: + - pattern-either: + - pattern: | + $T.autocorrectionType = .no + - pattern: | + $T.autocorrectionType = .No + - pattern-regex: 'UITextAutocorrectionTypeNo' + - pattern-regex: '\.disableAutocorrection\s*\(\s*true\s*\)' + - pattern-regex: '\.autocorrectionDisabled\s*\(\s*true\s*\)' + message: This app does not disable Keyboard cache. It must be disabled for all sensitive data inputs. + languages: + - swift + severity: INFO + metadata: + cwe: cwe-919 + masvs: storage-5 + owasp-mobile: m2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06d-Testing-Data-Storage.md#finding-sensitive-data-in-the-keyboard-cache-mstg-storage-5 diff --git a/mobsfscan/rules/semgrep/best_practices/swift/resilience.yaml b/mobsfscan/rules/semgrep/best_practices/swift/resilience.yaml new file mode 100644 index 0000000..61bede9 --- /dev/null +++ b/mobsfscan/rules/semgrep/best_practices/swift/resilience.yaml @@ -0,0 +1,29 @@ +rules: + - id: ios_detect_reversing + patterns: + - pattern-regex: '"FridaGadget"[\s\S]{0,5000}"cynject"[\s\S]{0,5000}"libcycript"[\s\S]{0,5000}"/usr/sbin/frida-server"' + message: This app does not have Reverse engineering detection capabilities. + languages: + - swift + severity: INFO + metadata: + owasp-mobile: m9 + masvs: resilience-4 + cwe: cwe-919 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06j-Testing-Resiliency-Against-Reverse-Engineering.md#ios-anti-reversing-defenses + - id: ios_cert_pinning + patterns: + - pattern-either: + - pattern-regex: 'PinnedCertificatesTrustEvaluator' + - pattern-regex: 'TrustKit\.initSharedInstance' + message: This app does not have Certificate Pinning implemented in code. + languages: + - swift + severity: INFO + metadata: + cwe: cwe-295 + masvs: network-4 + owasp-mobile: m3 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05g-Testing-Network-Communication.md#testing-custom-certificate-stores-and-certificate-pinning-mstg-network-4 diff --git a/mobsfscan/rules/semgrep/java/android/biometric_crypto.yaml b/mobsfscan/rules/semgrep/java/android/biometric_crypto.yaml new file mode 100644 index 0000000..d152339 --- /dev/null +++ b/mobsfscan/rules/semgrep/java/android/biometric_crypto.yaml @@ -0,0 +1,33 @@ +# Security concept informed by: +# https://github.com/mindedsecurity/semgrep-rules-android-security +rules: + - id: android_biometric_without_crypto + patterns: + - pattern-inside: | + class $CALLBACK extends BiometricPrompt.AuthenticationCallback { + ... + } + - pattern: | + void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult $RESULT) { + ... + } + - pattern-not: | + void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult $RESULT) { + ... + $RESULT.getCryptoObject(); + ... + } + message: >- + Biometric authentication succeeds without using the cryptographic object + from AuthenticationResult. Bind authentication to a Keystore-backed + cryptographic operation so the protected operation cannot proceed solely + from the callback result. + languages: + - java + severity: WARNING + metadata: + cwe: cwe-287 + owasp-mobile: m4 + masvs: auth-8 + reference: >- + https://mas.owasp.org/MASTG/tests/android/MASVS-AUTH/MASTG-TEST-0018/ diff --git a/mobsfscan/rules/semgrep/android/hidden_ui.yaml b/mobsfscan/rules/semgrep/java/android/hidden_ui.yaml similarity index 100% rename from mobsfscan/rules/semgrep/android/hidden_ui.yaml rename to mobsfscan/rules/semgrep/java/android/hidden_ui.yaml diff --git a/mobsfscan/rules/semgrep/android/logging.yaml b/mobsfscan/rules/semgrep/java/android/logging.yaml similarity index 100% rename from mobsfscan/rules/semgrep/android/logging.yaml rename to mobsfscan/rules/semgrep/java/android/logging.yaml diff --git a/mobsfscan/rules/semgrep/android/secrets.yaml b/mobsfscan/rules/semgrep/java/android/secrets.yaml similarity index 100% rename from mobsfscan/rules/semgrep/android/secrets.yaml rename to mobsfscan/rules/semgrep/java/android/secrets.yaml diff --git a/mobsfscan/rules/semgrep/java/android/sensitive_input.yaml b/mobsfscan/rules/semgrep/java/android/sensitive_input.yaml new file mode 100644 index 0000000..ae801bf --- /dev/null +++ b/mobsfscan/rules/semgrep/java/android/sensitive_input.yaml @@ -0,0 +1,27 @@ +# Security concept informed by: +# https://github.com/mindedsecurity/semgrep-rules-android-security +rules: + - id: android_sensitive_input_keyboard_cache + patterns: + - pattern: $FIELD.setInputType($INPUT_TYPE) + - metavariable-regex: + metavariable: $FIELD + regex: >- + (?i).*(?:password|passcode|pin|secret|otp|token).* + - metavariable-regex: + metavariable: $INPUT_TYPE + regex: >- + ^(?!.*(?:TYPE_TEXT_VARIATION_PASSWORD|TYPE_TEXT_VARIATION_VISIBLE_PASSWORD|TYPE_NUMBER_VARIATION_PASSWORD|TYPE_TEXT_FLAG_NO_SUGGESTIONS)).*$ + message: >- + A sensitive input field is configured without a password variation or + TYPE_TEXT_FLAG_NO_SUGGESTIONS. Disable suggestions for sensitive input + to reduce exposure through keyboard learning and suggestion history. + languages: + - java + severity: WARNING + metadata: + cwe: cwe-524 + owasp-mobile: m1 + masvs: storage-5 + reference: >- + https://mas.owasp.org/MASTG/tests/android/MASVS-STORAGE/MASTG-TEST-0005/ diff --git a/mobsfscan/rules/semgrep/java/android/sensitive_notification.yaml b/mobsfscan/rules/semgrep/java/android/sensitive_notification.yaml new file mode 100644 index 0000000..470f7c7 --- /dev/null +++ b/mobsfscan/rules/semgrep/java/android/sensitive_notification.yaml @@ -0,0 +1,27 @@ +# Security concept informed by: +# https://github.com/mindedsecurity/semgrep-rules-android-security +rules: + - id: android_sensitive_notification + patterns: + - pattern-either: + - pattern: $BUILDER.setContentText($DATA) + - pattern: $BUILDER.setContentTitle($DATA) + - pattern: $BUILDER.setSubText($DATA) + - pattern: $BUILDER.setTicker($DATA) + - metavariable-regex: + metavariable: $DATA + regex: >- + (?i).*(?:password|passcode|pin|secret|otp|token|auth(?:entication)?code).* + message: >- + Secret-like data is displayed in a notification. Notifications can + expose content on the lock screen or to notification listeners. Avoid + placing credentials, one-time codes, tokens, or other secrets in them. + languages: + - java + severity: WARNING + metadata: + cwe: cwe-200 + owasp-mobile: m1 + masvs: storage-7 + reference: >- + https://mas.owasp.org/MASTG/tests/android/MASVS-STORAGE/MASTG-TEST-0010/ diff --git a/mobsfscan/rules/semgrep/android/word_readable_writable.yaml b/mobsfscan/rules/semgrep/java/android/word_readable_writable.yaml similarity index 100% rename from mobsfscan/rules/semgrep/android/word_readable_writable.yaml rename to mobsfscan/rules/semgrep/java/android/word_readable_writable.yaml diff --git a/mobsfscan/rules/semgrep/crypto/aes_ecb.yaml b/mobsfscan/rules/semgrep/java/crypto/aes_ecb.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/aes_ecb.yaml rename to mobsfscan/rules/semgrep/java/crypto/aes_ecb.yaml diff --git a/mobsfscan/rules/semgrep/crypto/aes_encryption_keys.yaml b/mobsfscan/rules/semgrep/java/crypto/aes_encryption_keys.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/aes_encryption_keys.yaml rename to mobsfscan/rules/semgrep/java/crypto/aes_encryption_keys.yaml diff --git a/mobsfscan/rules/semgrep/crypto/cbc_padding_oracle.yaml b/mobsfscan/rules/semgrep/java/crypto/cbc_padding_oracle.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/cbc_padding_oracle.yaml rename to mobsfscan/rules/semgrep/java/crypto/cbc_padding_oracle.yaml diff --git a/mobsfscan/rules/semgrep/crypto/cbc_static_iv.yaml b/mobsfscan/rules/semgrep/java/crypto/cbc_static_iv.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/cbc_static_iv.yaml rename to mobsfscan/rules/semgrep/java/crypto/cbc_static_iv.yaml diff --git a/mobsfscan/rules/semgrep/java/crypto/custom_xor_crypto.yaml b/mobsfscan/rules/semgrep/java/crypto/custom_xor_crypto.yaml new file mode 100644 index 0000000..16dc8d4 --- /dev/null +++ b/mobsfscan/rules/semgrep/java/crypto/custom_xor_crypto.yaml @@ -0,0 +1,29 @@ +# Security concept informed by: +# https://github.com/mindedsecurity/semgrep-rules-android-security +rules: + - id: android_custom_xor_crypto + patterns: + - pattern-inside: | + $RET $METHOD(...) { + ... + } + - metavariable-regex: + metavariable: $METHOD + regex: (?i).*(?:encrypt|decrypt|crypt).* + - pattern-either: + - pattern: $LEFT ^ $RIGHT + - pattern: $VALUE ^= $KEY + message: >- + A cryptography-named method uses XOR directly. Custom cryptographic + schemes are difficult to validate and commonly fail to provide + confidentiality or integrity. Use a standard authenticated-encryption + construction from a supported cryptographic provider. + languages: + - java + severity: WARNING + metadata: + cwe: cwe-327 + owasp-mobile: m5 + masvs: crypto-2 + reference: >- + https://mas.owasp.org/MASTG/tests/android/MASVS-CRYPTO/MASTG-TEST-0013/ diff --git a/mobsfscan/rules/semgrep/crypto/insecure_random.yaml b/mobsfscan/rules/semgrep/java/crypto/insecure_random.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/insecure_random.yaml rename to mobsfscan/rules/semgrep/java/crypto/insecure_random.yaml diff --git a/mobsfscan/rules/semgrep/crypto/insecure_ssl_v3.yaml b/mobsfscan/rules/semgrep/java/crypto/insecure_ssl_v3.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/insecure_ssl_v3.yaml rename to mobsfscan/rules/semgrep/java/crypto/insecure_ssl_v3.yaml diff --git a/mobsfscan/rules/semgrep/crypto/rsa_no_oeap.yaml b/mobsfscan/rules/semgrep/java/crypto/rsa_no_oeap.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/rsa_no_oeap.yaml rename to mobsfscan/rules/semgrep/java/crypto/rsa_no_oeap.yaml diff --git a/mobsfscan/rules/semgrep/crypto/sha1_hash.yaml b/mobsfscan/rules/semgrep/java/crypto/sha1_hash.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/sha1_hash.yaml rename to mobsfscan/rules/semgrep/java/crypto/sha1_hash.yaml diff --git a/mobsfscan/rules/semgrep/crypto/weak_ciphers.yaml b/mobsfscan/rules/semgrep/java/crypto/weak_ciphers.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/weak_ciphers.yaml rename to mobsfscan/rules/semgrep/java/crypto/weak_ciphers.yaml diff --git a/mobsfscan/rules/semgrep/crypto/weak_hashes.yaml b/mobsfscan/rules/semgrep/java/crypto/weak_hashes.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/weak_hashes.yaml rename to mobsfscan/rules/semgrep/java/crypto/weak_hashes.yaml diff --git a/mobsfscan/rules/semgrep/crypto/weak_iv.yaml b/mobsfscan/rules/semgrep/java/crypto/weak_iv.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/weak_iv.yaml rename to mobsfscan/rules/semgrep/java/crypto/weak_iv.yaml diff --git a/mobsfscan/rules/semgrep/crypto/weak_key_size.yaml b/mobsfscan/rules/semgrep/java/crypto/weak_key_size.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/weak_key_size.yaml rename to mobsfscan/rules/semgrep/java/crypto/weak_key_size.yaml diff --git a/mobsfscan/rules/semgrep/deserialization/jackson_deserialization.yaml b/mobsfscan/rules/semgrep/java/deserialization/jackson_deserialization.yaml similarity index 100% rename from mobsfscan/rules/semgrep/deserialization/jackson_deserialization.yaml rename to mobsfscan/rules/semgrep/java/deserialization/jackson_deserialization.yaml diff --git a/mobsfscan/rules/semgrep/deserialization/object_deserialization.yaml b/mobsfscan/rules/semgrep/java/deserialization/object_deserialization.yaml similarity index 100% rename from mobsfscan/rules/semgrep/deserialization/object_deserialization.yaml rename to mobsfscan/rules/semgrep/java/deserialization/object_deserialization.yaml diff --git a/mobsfscan/rules/semgrep/injection/command_injection.yaml b/mobsfscan/rules/semgrep/java/injection/command_injection.yaml similarity index 100% rename from mobsfscan/rules/semgrep/injection/command_injection.yaml rename to mobsfscan/rules/semgrep/java/injection/command_injection.yaml diff --git a/mobsfscan/rules/semgrep/injection/command_injection_formated.yaml b/mobsfscan/rules/semgrep/java/injection/command_injection_formated.yaml similarity index 100% rename from mobsfscan/rules/semgrep/injection/command_injection_formated.yaml rename to mobsfscan/rules/semgrep/java/injection/command_injection_formated.yaml diff --git a/mobsfscan/rules/semgrep/injection/sqlite_injection.yaml b/mobsfscan/rules/semgrep/java/injection/sqlite_injection.yaml similarity index 100% rename from mobsfscan/rules/semgrep/injection/sqlite_injection.yaml rename to mobsfscan/rules/semgrep/java/injection/sqlite_injection.yaml diff --git a/mobsfscan/rules/semgrep/network/accept_self_signed.yaml b/mobsfscan/rules/semgrep/java/network/accept_self_signed.yaml similarity index 100% rename from mobsfscan/rules/semgrep/network/accept_self_signed.yaml rename to mobsfscan/rules/semgrep/java/network/accept_self_signed.yaml diff --git a/mobsfscan/rules/semgrep/network/default_http_client_tls.yaml b/mobsfscan/rules/semgrep/java/network/default_http_client_tls.yaml similarity index 100% rename from mobsfscan/rules/semgrep/network/default_http_client_tls.yaml rename to mobsfscan/rules/semgrep/java/network/default_http_client_tls.yaml diff --git a/mobsfscan/rules/semgrep/java/network/weak_tls_configuration.yaml b/mobsfscan/rules/semgrep/java/network/weak_tls_configuration.yaml new file mode 100644 index 0000000..b790240 --- /dev/null +++ b/mobsfscan/rules/semgrep/java/network/weak_tls_configuration.yaml @@ -0,0 +1,44 @@ +# Security concepts informed by: +# https://github.com/mindedsecurity/semgrep-rules-android-security +rules: + - id: insecure_tls_version + patterns: + - pattern-either: + - pattern: $C.getInstance("TLSv1") + - pattern: $C.getInstance("TLSv1.0") + - pattern: $C.getInstance("TLSv1.1") + - pattern: $S.setEnabledProtocols(new String[] { ..., "TLSv1", ... }) + - pattern: $S.setEnabledProtocols(new String[] { ..., "TLSv1.0", ... }) + - pattern: $S.setEnabledProtocols(new String[] { ..., "TLSv1.1", ... }) + message: >- + TLS 1.0 and TLS 1.1 are deprecated and have known weaknesses. Use TLS + 1.2 or TLS 1.3 and avoid explicitly enabling older protocol versions. + languages: + - java + severity: ERROR + metadata: + cwe: cwe-326 + owasp-mobile: m5 + masvs: network-2 + reference: >- + https://mas.owasp.org/MASTG/tests/android/MASVS-NETWORK/MASTG-TEST-0020/ + - id: weak_tls_cipher_suite + patterns: + - pattern: $S.setEnabledCipherSuites($SUITES) + - metavariable-regex: + metavariable: $SUITES + regex: >- + (?i).*(?:_NULL_|_ANON_|_EXPORT_|_RC4_|_DES_|3DES|_MD5).* + message: >- + The explicitly enabled TLS cipher suites include a null, anonymous, + export-grade, RC4, DES/3DES, or MD5-based suite. Remove weak suites and + rely on current platform defaults or a modern restricted configuration. + languages: + - java + severity: ERROR + metadata: + cwe: cwe-327 + owasp-mobile: m5 + masvs: network-2 + reference: >- + https://mas.owasp.org/MASTG/tests/android/MASVS-NETWORK/MASTG-TEST-0020/ diff --git a/mobsfscan/rules/semgrep/webview/webview_allow_file_from_url.yaml b/mobsfscan/rules/semgrep/java/webview/webview_allow_file_from_url.yaml similarity index 100% rename from mobsfscan/rules/semgrep/webview/webview_allow_file_from_url.yaml rename to mobsfscan/rules/semgrep/java/webview/webview_allow_file_from_url.yaml diff --git a/mobsfscan/rules/semgrep/webview/webview_debugging.yaml b/mobsfscan/rules/semgrep/java/webview/webview_debugging.yaml similarity index 100% rename from mobsfscan/rules/semgrep/webview/webview_debugging.yaml rename to mobsfscan/rules/semgrep/java/webview/webview_debugging.yaml diff --git a/mobsfscan/rules/semgrep/webview/webview_external_storage.yaml b/mobsfscan/rules/semgrep/java/webview/webview_external_storage.yaml similarity index 100% rename from mobsfscan/rules/semgrep/webview/webview_external_storage.yaml rename to mobsfscan/rules/semgrep/java/webview/webview_external_storage.yaml diff --git a/mobsfscan/rules/semgrep/webview/webview_file_access.yaml b/mobsfscan/rules/semgrep/java/webview/webview_file_access.yaml similarity index 100% rename from mobsfscan/rules/semgrep/webview/webview_file_access.yaml rename to mobsfscan/rules/semgrep/java/webview/webview_file_access.yaml diff --git a/mobsfscan/rules/semgrep/webview/webview_ignore_ssl_errors.yaml b/mobsfscan/rules/semgrep/java/webview/webview_ignore_ssl_errors.yaml similarity index 100% rename from mobsfscan/rules/semgrep/webview/webview_ignore_ssl_errors.yaml rename to mobsfscan/rules/semgrep/java/webview/webview_ignore_ssl_errors.yaml diff --git a/mobsfscan/rules/semgrep/webview/webview_javascript_interface.yaml b/mobsfscan/rules/semgrep/java/webview/webview_javascript_interface.yaml similarity index 100% rename from mobsfscan/rules/semgrep/webview/webview_javascript_interface.yaml rename to mobsfscan/rules/semgrep/java/webview/webview_javascript_interface.yaml diff --git a/mobsfscan/rules/semgrep/java/webview/webview_mixed_content.yaml b/mobsfscan/rules/semgrep/java/webview/webview_mixed_content.yaml new file mode 100644 index 0000000..7979394 --- /dev/null +++ b/mobsfscan/rules/semgrep/java/webview/webview_mixed_content.yaml @@ -0,0 +1,26 @@ +rules: + - id: webview_mixed_content + patterns: + - pattern-either: + - pattern: | + $W.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW) + - pattern: | + setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW) + - pattern: | + $W.setMixedContentMode($S.MIXED_CONTENT_ALWAYS_ALLOW) + - pattern: | + setMixedContentMode($S.MIXED_CONTENT_ALWAYS_ALLOW) + message: >- + Insecure WebView Implementation. WebView is configured with + MIXED_CONTENT_ALWAYS_ALLOW, allowing a page loaded over HTTPS to load + content from insecure HTTP origins. This exposes the application to + man-in-the-middle content injection. + languages: + - java + severity: ERROR + metadata: + cwe: cwe-319 + owasp-mobile: m3 + masvs: network-1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05g-Testing-Network-Communication.md diff --git a/mobsfscan/rules/semgrep/xxe/xmldecoder_xxe.yaml b/mobsfscan/rules/semgrep/java/xxe/xmldecoder_xxe.yaml similarity index 100% rename from mobsfscan/rules/semgrep/xxe/xmldecoder_xxe.yaml rename to mobsfscan/rules/semgrep/java/xxe/xmldecoder_xxe.yaml diff --git a/mobsfscan/rules/semgrep/xxe/xmlfactory_external_entities_enabled.yaml b/mobsfscan/rules/semgrep/java/xxe/xmlfactory_external_entities_enabled.yaml similarity index 100% rename from mobsfscan/rules/semgrep/xxe/xmlfactory_external_entities_enabled.yaml rename to mobsfscan/rules/semgrep/java/xxe/xmlfactory_external_entities_enabled.yaml diff --git a/mobsfscan/rules/semgrep/xxe/xmlfactory_xxe.yaml b/mobsfscan/rules/semgrep/java/xxe/xmlfactory_xxe.yaml similarity index 100% rename from mobsfscan/rules/semgrep/xxe/xmlfactory_xxe.yaml rename to mobsfscan/rules/semgrep/java/xxe/xmlfactory_xxe.yaml diff --git a/mobsfscan/rules/semgrep/kotlin/android.yaml b/mobsfscan/rules/semgrep/kotlin/android.yaml new file mode 100644 index 0000000..a0cf033 --- /dev/null +++ b/mobsfscan/rules/semgrep/kotlin/android.yaml @@ -0,0 +1,186 @@ +rules: + - id: android_kotlin_hiddenui + patterns: + - pattern-either: + - pattern: | + $V.visibility = View.GONE + - pattern: | + $V.visibility = View.INVISIBLE + - pattern: | + $V.visibility = if ($C) View.GONE else $E + - pattern: | + $V.visibility = if ($C) View.INVISIBLE else $E + - pattern: | + $V.setVisibility(View.GONE) + - pattern: | + $V.setVisibility(View.INVISIBLE) + message: >- + Hidden elements in view can be used to hide data from user. But this data + can be leaked. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-919 + owasp-mobile: m1 + masvs: storage-7 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#checking-for-sensitive-data-disclosure-through-the-user-interface-mstg-storage-7 + - id: android_kotlin_logging + patterns: + - pattern-either: + - pattern: | + Log.$M(...) + - pattern: | + System.out.print(...) + - pattern: | + System.out.println(...) + - pattern: | + System.err.print(...) + - pattern: | + System.err.println(...) + message: The App logs information. Sensitive information should never be logged. + languages: + - kotlin + severity: INFO + metadata: + cwe: cwe-532 + owasp-mobile: m1 + masvs: storage-3 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#logs + - id: android_kotlin_hardcoded + patterns: + - pattern-either: + - pattern: | + $X = "..." + - pattern: | + val $X = "..." + - pattern: | + var $X = "..." + - pattern: | + const val $X = "..." + - pattern-not: | + $X = "" + - pattern-not: | + val $X = "" + - pattern-not: | + var $X = "" + - metavariable-regex: + metavariable: $X + regex: '(?i)^(?:password|pass|username|secret|key|(?:api|secret|private|access|encryption|auth)_?key)$' + message: >- + Files may contain hardcoded sensitive information like usernames, + passwords, keys etc. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-798 + owasp-mobile: m9 + masvs: storage-14 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#storing-a-key---example + - id: android_kotlin_world_writable + patterns: + - pattern-either: + - pattern: | + Context.MODE_WORLD_WRITEABLE + - pattern: | + $C.getSharedPreferences($N, 2) + - pattern: | + $C.openFileOutput($N, 2) + message: The file is World Writable. Any App can write to the file + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-276 + owasp-mobile: m2 + masvs: storage-2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#testing-local-storage-for-sensitive-data-mstg-storage-1-and-mstg-storage-2 + - id: android_kotlin_world_readable + patterns: + - pattern-either: + - pattern: | + Context.MODE_WORLD_READABLE + - pattern: | + $C.getSharedPreferences($N, 1) + - pattern: | + $C.openFileOutput($N, 1) + message: The file is World Readable. Any App can read from the file + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-276 + owasp-mobile: m2 + masvs: storage-2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#testing-local-storage-for-sensitive-data-mstg-storage-1-and-mstg-storage-2 + - id: android_kotlin_world_read_write + patterns: + - pattern: | + $C.openFileOutput($N, 3) + message: The file is World Readable and Writable. Any App can read/write to the file + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-276 + owasp-mobile: m2 + masvs: storage-2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#testing-local-storage-for-sensitive-data-mstg-storage-1-and-mstg-storage-2 + - id: android_kotlin_sensitive_input_keyboard_cache + patterns: + - pattern-either: + - pattern: | + $F.setInputType($T) + - pattern: | + $F.inputType = $T + - metavariable-regex: + metavariable: $F + regex: (?i).*(?:password|passcode|pin|secret|otp|token).* + - metavariable-regex: + metavariable: $T + regex: '^(?!.*(?:TYPE_TEXT_VARIATION_PASSWORD|TYPE_TEXT_VARIATION_VISIBLE_PASSWORD|TYPE_NUMBER_VARIATION_PASSWORD|TYPE_TEXT_FLAG_NO_SUGGESTIONS)).*$' + message: >- + A sensitive input field is configured without a password variation or + TYPE_TEXT_FLAG_NO_SUGGESTIONS. Disable suggestions for sensitive input. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-524 + owasp-mobile: m1 + masvs: storage-5 + reference: >- + https://mas.owasp.org/MASTG/tests/android/MASVS-STORAGE/MASTG-TEST-0005/ + - id: android_kotlin_sensitive_notification + patterns: + - pattern-either: + - pattern: | + $B.setContentText($D) + - pattern: | + $B.setContentTitle($D) + - pattern: | + $B.setSubText($D) + - pattern: | + $B.setTicker($D) + - metavariable-regex: + metavariable: $D + regex: (?i).*(?:password|passcode|pin|secret|otp|token|auth(?:entication)?code).* + message: >- + Secret-like data is displayed in a notification and may be exposed on the + lock screen or to notification listeners. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-200 + owasp-mobile: m1 + masvs: storage-7 + reference: >- + https://mas.owasp.org/MASTG/tests/android/MASVS-STORAGE/MASTG-TEST-0010/ diff --git a/mobsfscan/rules/semgrep/kotlin/biometric.yaml b/mobsfscan/rules/semgrep/kotlin/biometric.yaml new file mode 100644 index 0000000..8300099 --- /dev/null +++ b/mobsfscan/rules/semgrep/kotlin/biometric.yaml @@ -0,0 +1,37 @@ +rules: + - id: android_kotlin_biometric_without_crypto + patterns: + - pattern-inside: | + class $CALLBACK : BiometricPrompt.AuthenticationCallback() { + ... + } + - pattern: | + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + ... + } + - pattern-not: | + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + ... + result.cryptoObject + ... + } + - pattern-not: | + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + ... + result.getCryptoObject() + ... + } + message: >- + Biometric authentication succeeds without using the cryptographic object + from AuthenticationResult. Bind authentication to a Keystore-backed + cryptographic operation so the protected operation cannot proceed solely + from the callback result. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-287 + owasp-mobile: m4 + masvs: auth-8 + reference: >- + https://mas.owasp.org/MASTG/tests/android/MASVS-AUTH/MASTG-TEST-0018/ diff --git a/mobsfscan/rules/semgrep/kotlin/crypto.yaml b/mobsfscan/rules/semgrep/kotlin/crypto.yaml new file mode 100644 index 0000000..c6008c1 --- /dev/null +++ b/mobsfscan/rules/semgrep/kotlin/crypto.yaml @@ -0,0 +1,330 @@ +rules: + - id: android_kotlin_insecure_random + patterns: + - pattern-either: + - pattern: | + java.util.Random(...) + - pattern: | + Random() + message: The App uses an insecure Random Number Generator. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-330 + owasp-mobile: m5 + masvs: crypto-6 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#weak-random-number-generators + - id: android_kotlin_aes_ecb + patterns: + - pattern: | + Cipher.getInstance("=~/AES\/ECB.*/i") + message: >- + The App uses ECB mode in Cryptographic encryption algorithm. ECB mode is + known to be weak as it results in the same ciphertext for identical blocks + of plaintext. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-327 + owasp-mobile: m5 + masvs: crypto-2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#weak-block-cipher-mode + - id: android_kotlin_aes_ecb_default + patterns: + - pattern: | + Cipher.getInstance("AES") + message: >- + Calling Cipher.getInstance("AES") will return AES ECB mode by default. ECB mode is + known to be weak as it results in the same ciphertext for identical blocks + of plaintext. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-327 + owasp-mobile: m5 + masvs: crypto-2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#weak-block-cipher-mode + - id: cbc_kotlin_padding_oracle + patterns: + - pattern-either: + - pattern: | + Cipher.getInstance("=~/.*\/CBC\/PKCS5Padding.*/i") + - pattern: | + Cipher.getInstance("=~/.*\/CBC\/PKCS7Padding.*/i") + message: The App uses the encryption mode CBC with PKCS5/PKCS7 padding. This configuration is vulnerable to padding oracle attacks. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-649 + owasp-mobile: m5 + masvs: crypto-3 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 + - id: android_kotlin_rsa_no_oaep + patterns: + - pattern: | + Cipher.getInstance("=~/rsa\/.{1,48}\/nopadding/i") + message: >- + This App uses RSA Crypto without OAEP padding. The purpose of the padding + scheme is to prevent a number of attacks on RSA that only work when the + encryption is performed without padding. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-780 + owasp-mobile: m5 + masvs: crypto-3 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#mobile-app-cryptography + - id: android_kotlin_weak_hash + patterns: + - pattern: | + $D.getInstance("=~/MD4/i") + message: Weak Hash algorithm used. The hash algorithm is known to have hash collisions. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-327 + owasp-mobile: m5 + masvs: crypto-4 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 + - id: android_kotlin_weak_ciphers + patterns: + - pattern-either: + - pattern: | + Cipher.getInstance("=~/(?i)(des|desede|rc2|rc4|blowfish).*/") + message: Weak Encryption algorithm used + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-327 + owasp-mobile: m5 + masvs: crypto-4 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 + - id: android_kotlin_md5 + patterns: + - pattern-either: + - pattern: | + $D.getInstance("=~/MD5/i") + - pattern: | + DigestUtils.md5(...) + message: MD5 is a weak hash known to have hash collisions. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-327 + owasp-mobile: m5 + masvs: crypto-4 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 + - id: android_kotlin_sha1 + patterns: + - pattern-either: + - pattern: | + $D.getInstance("=~/SHA-?1/i") + - pattern: | + DigestUtils.sha(...) + message: SHA-1 is a weak hash known to have hash collisions. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-327 + owasp-mobile: m5 + masvs: crypto-4 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 + - id: android_kotlin_weak_iv + patterns: + - pattern-either: + - pattern-regex: '0x00\s*,\s*0x00\s*,\s*0x00\s*,\s*0x00\s*,\s*0x00\s*,\s*0x00\s*,\s*0x00\s*,\s*0x00' + - pattern-regex: '0x01\s*,\s*0x02\s*,\s*0x03\s*,\s*0x04\s*,\s*0x05\s*,\s*0x06\s*,\s*0x07' + message: >- + The App may use weak IVs like "0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00" or + "0x01,0x02,0x03,0x04,0x05,0x06,0x07". Not using a random IV makes the + resulting ciphertext much more predictable and susceptible to a dictionary + attack. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-1204 + owasp-mobile: m5 + masvs: crypto-5 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#mobile-app-cryptography + - id: android_kotlin_custom_xor_crypto + patterns: + - pattern-either: + - pattern: | + fun $N(...) = $A xor $B + - pattern: | + fun $N(...): $R = $A xor $B + - pattern: | + fun $N(...) { + ... + $A xor $B + ... + } + - pattern: | + fun $N(...) { + ... + $A.xor($B) + ... + } + - metavariable-regex: + metavariable: $N + regex: (?i).*(?:encrypt|decrypt|crypt).* + message: >- + A cryptography-named function uses XOR directly. Use a standard + authenticated-encryption construction instead of custom cryptography. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-327 + owasp-mobile: m5 + masvs: crypto-2 + reference: >- + https://mas.owasp.org/MASTG/tests/android/MASVS-CRYPTO/MASTG-TEST-0013/ + - id: android_kotlin_aes_hardcoded_key + patterns: + - pattern-either: + - pattern: | + val $S = SecretKeySpec("...".toByteArray(), "AES") + ... + $C.init(..., $S) + - pattern: | + val $S = SecretKeySpec("...".getBytes(), "AES") + ... + $C.init(..., $S) + - pattern: | + val $P = "..." + ... + val $S = SecretKeySpec($P.toByteArray(), "AES") + ... + $C.init(..., $S) + message: >- + Hardcoded encryption key makes AES symmetric encryption useless. An + attacker can easily reverse engineer the application and recover the keys. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-321 + owasp-mobile: m5 + masvs: crypto-1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#common-configuration-issues-mstg-crypto-1-mstg-crypto-2-and-mstg-crypto-3 + - id: android_kotlin_cbc_static_iv + patterns: + - pattern-either: + - pattern: | + val $X = "...".toByteArray() + ... + val $Y = IvParameterSpec($X, ...) + ... + Cipher.getInstance("=~/AES/CBC.*/i") + - pattern: | + val $X = "...".getBytes() + ... + val $Y = IvParameterSpec($X, ...) + ... + Cipher.getInstance("=~/AES/CBC.*/i") + - pattern: | + val $X = byteArrayOf(...) + ... + val $Y = IvParameterSpec($X, ...) + ... + Cipher.getInstance("=~/AES/CBC.*/i") + message: >- + The IV for AES CBC mode should be random. A static IV makes the ciphertext + vulnerable to Chosen Plaintext Attack. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-329 + owasp-mobile: m5 + masvs: crypto-5 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#predictable-initialization-vector + - id: android_kotlin_insecure_sslv3 + patterns: + - pattern: | + SSLContext.getInstance("SSLv3") + message: SSLv3 is insecure and has multiple known vulnerabilities. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-327 + owasp-mobile: m5 + masvs: crypto-4 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 + - id: android_kotlin_weak_key_size + patterns: + - pattern-either: + - pattern: | + val $K = KeyPairGenerator.getInstance("RSA") + ... + $K.initialize(1024) + - pattern: | + val $K = KeyPairGenerator.getInstance("RSA") + ... + $K.initialize(512) + - pattern: | + val $K = KeyPairGenerator.getInstance("EC") + ... + $K.initialize(ECGenParameterSpec("secp112r1")) + - pattern: | + val $K = KeyPairGenerator.getInstance("EC") + ... + val $S = ECGenParameterSpec("secp112r1") + ... + $K.initialize($S) + - pattern: | + val $K = KeyPairGenerator.getInstance("EC") + ... + $K.initialize(ECGenParameterSpec("secp224r1")) + - pattern: | + val $K = KeyPairGenerator.getInstance("EC") + ... + val $S = ECGenParameterSpec("secp224r1") + ... + $K.initialize($S) + - pattern: | + val $K = KeyGenerator.getInstance("Blowfish") + ... + $K.init(64) + - pattern: | + val $K = KeyGenerator.getInstance("AES") + ... + $K.init(64) + message: >- + Cryptographic implementations with insufficient key length is susceptible + to bruteforce attacks. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-326 + owasp-mobile: m5 + masvs: crypto-3 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#common-configuration-issues-mstg-crypto-1-mstg-crypto-2-and-mstg-crypto-3 + diff --git a/mobsfscan/rules/semgrep/kotlin/injection.yaml b/mobsfscan/rules/semgrep/kotlin/injection.yaml new file mode 100644 index 0000000..17a7f7d --- /dev/null +++ b/mobsfscan/rules/semgrep/kotlin/injection.yaml @@ -0,0 +1,189 @@ +rules: + - id: android_kotlin_sql_raw_query + patterns: + - pattern-either: + - pattern: | + $D.rawQuery(...) + - pattern: | + $D.execSQL(...) + message: >- + App uses SQLite Database and execute raw SQL query. Untrusted user input in + raw SQL queries can cause SQL Injection. Also sensitive information should + be encrypted and written to the database. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-78 + owasp-mobile: m7 + masvs: platform-2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04h-Testing-Code-Quality.md#injection-flaws-mstg-arch-2-and-mstg-platform-2 + - id: android_kotlin_jackson_deserialize + patterns: + - pattern: | + $M.enableDefaultTyping(...) + message: >- + The app uses jackson deserialization library. Deserialization of untrusted + input can result in arbitrary code execution. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-502 + owasp-mobile: m7 + masvs: platform-8 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05h-Testing-Platform-Interaction.md#testing-object-persistence-mstg-platform-8 + - id: android_kotlin_command_injection + patterns: + - pattern-not: | + Runtime.getRuntime().exec("...") + - pattern-not: | + Runtime.getRuntime().exec(arrayOf("...", ...)) + - pattern: | + Runtime.getRuntime().exec(...) + message: User controlled strings in exec() will result in command execution. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-78 + owasp-mobile: m7 + masvs: platform-2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04h-Testing-Code-Quality.md#injection-flaws-mstg-arch-2-and-mstg-platform-2 + - id: android_kotlin_command_injection_warning + patterns: + - pattern-either: + - pattern: | + $RUNTIME.exec($X + $Y) + - pattern: | + $RUNTIME.exec(String.format(...)) + - pattern: | + $RUNTIME.loadLibrary($X + $Y) + - pattern: | + $RUNTIME.loadLibrary(String.format(...)) + - pattern: | + $RUNTIME.exec("=~/(sh|bash|ksh|csh|tcsh|zsh)/", "-c", $ARG, ...) + - pattern: | + $RUNTIME.exec(arrayOf("=~/(sh|bash|ksh|csh|tcsh|zsh)/", "-c", $ARG, ...)) + - pattern-inside: | + val $RUNTIME = Runtime.getRuntime() + ... + message: >- + A formatted or concatenated string was detected as input to a + java.lang.Runtime call. This is dangerous if a variable is controlled by + user input and could result in a command injection. Ensure your variables + are not controlled by users or sufficiently sanitized. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-78 + owasp-mobile: m7 + masvs: platform-2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04h-Testing-Code-Quality.md#injection-flaws-mstg-arch-2-and-mstg-platform-2 + - id: android_kotlin_object_deserialization + patterns: + - pattern: | + ObjectInputStream(...) + message: >- + Found object deserialization using ObjectInputStream. Deserializing entire + Java objects is dangerous because malicious actors can create Java object + streams with unintended consequences. Ensure that the objects being + deserialized are not user-controlled. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-502 + owasp-mobile: m1 + masvs: platform-8 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05h-Testing-Platform-Interaction.md#testing-object-persistence-mstg-platform-8 + - id: android_kotlin_xmlinputfactory_xxe + patterns: + - pattern-not-inside: | + fun $METHOD(...) { + ... + $XMLFACTORY.setProperty("javax.xml.stream.isSupportingExternalEntities", false) + ... + } + - pattern-either: + - pattern: | + val $XMLFACTORY = XMLInputFactory.newFactory(...) + - pattern: | + val $XMLFACTORY = XMLInputFactory.newInstance(...) + - pattern: | + XMLInputFactory.newFactory(...) + - pattern: | + XMLInputFactory.newInstance(...) + message: >- + XML external entities are not explicitly disabled for this + XMLInputFactory. This could be vulnerable to XML external entity + vulnerabilities. Explicitly disable external entities by setting + "javax.xml.stream.isSupportingExternalEntities" to false. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-611 + owasp-mobile: m8 + masvs: platform-2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04h-Testing-Code-Quality.md#injection-flaws-mstg-arch-2-and-mstg-platform-2 + - id: android_kotlin_xmlinputfactory_xxe_enabled + patterns: + - pattern: | + $XMLFACTORY.setProperty("javax.xml.stream.isSupportingExternalEntities", true) + message: >- + XML external entities are enabled for this XMLInputFactory. This is + vulnerable to XML external entity attacks. Disable external entities by + setting "javax.xml.stream.isSupportingExternalEntities" to false. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-611 + owasp-mobile: m8 + masvs: platform-2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04h-Testing-Code-Quality.md#injection-flaws-mstg-arch-2-and-mstg-platform-2 + - id: android_kotlin_xml_decoder_xxe + patterns: + - pattern: | + fun $METHOD(...): $R { + ... + XMLDecoder(...) + ... + } + - pattern-not: | + fun $METHOD(...): $R { + ... + XMLDecoder("...") + ... + } + - pattern-not: | + fun $METHOD(...): $R { + ... + val $STR = "..." + ... + XMLDecoder($STR) + ... + } + message: >- + XMLDecoder should not be used to parse untrusted data. + Deserializing user input can lead to arbitrary code execution. + Use an alternative and explicitly disable external entities. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-611 + owasp-mobile: m8 + masvs: platform-2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04h-Testing-Code-Quality.md#injection-flaws-mstg-arch-2-and-mstg-platform-2 + diff --git a/mobsfscan/rules/semgrep/kotlin/network.yaml b/mobsfscan/rules/semgrep/kotlin/network.yaml new file mode 100644 index 0000000..869f0ea --- /dev/null +++ b/mobsfscan/rules/semgrep/kotlin/network.yaml @@ -0,0 +1,77 @@ +rules: + - id: android_kotlin_insecure_ssl + patterns: + - pattern-either: + - pattern: | + $X.ALLOW_ALL_HOSTNAME_VERIFIER + - pattern: | + $X.setDefaultHostnameVerifier(...) + - pattern: | + NullHostnameVerifier(...) + - pattern: | + TrustAllSSLSocketFactory(...) + - pattern: | + AllTrustSSLSocketFactory(...) + - pattern: | + NonValidatingSSLSocketFactory(...) + - pattern: | + SSLCertificateSocketFactory(...) + message: >- + Insecure Implementation of SSL. Trusting all the certificates or accepting + self signed certificates is a critical Security Hole. This application is + vulnerable to MITM attacks + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-295 + owasp-mobile: m3 + masvs: network-3 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05g-Testing-Network-Communication.md#android-network-apis + - id: android_kotlin_insecure_tls_version + patterns: + - pattern-either: + - pattern: | + SSLContext.getInstance("TLSv1") + - pattern: | + SSLContext.getInstance("TLSv1.0") + - pattern: | + SSLContext.getInstance("TLSv1.1") + - pattern: | + $S.setEnabledProtocols(arrayOf(..., "TLSv1", ...)) + - pattern: | + $S.setEnabledProtocols(arrayOf(..., "TLSv1.0", ...)) + - pattern: | + $S.setEnabledProtocols(arrayOf(..., "TLSv1.1", ...)) + message: >- + TLS 1.0 and TLS 1.1 are deprecated and have known weaknesses. Use TLS 1.2 + or TLS 1.3 and avoid explicitly enabling older protocol versions. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-326 + owasp-mobile: m5 + masvs: network-2 + reference: >- + https://mas.owasp.org/MASTG/tests/android/MASVS-NETWORK/MASTG-TEST-0020/ + - id: android_kotlin_weak_tls_cipher_suite + patterns: + - pattern: | + $S.setEnabledCipherSuites($A) + - metavariable-regex: + metavariable: $A + regex: (?i).*(?:_NULL_|_ANON_|_EXPORT_|_RC4_|_DES_|3DES|_MD5).* + message: >- + The explicitly enabled TLS cipher suites include a null, anonymous, + export-grade, RC4, DES/3DES, or MD5-based suite. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-327 + owasp-mobile: m5 + masvs: network-2 + reference: >- + https://mas.owasp.org/MASTG/tests/android/MASVS-NETWORK/MASTG-TEST-0020/ diff --git a/mobsfscan/rules/semgrep/kotlin/webview.yaml b/mobsfscan/rules/semgrep/kotlin/webview.yaml new file mode 100644 index 0000000..b689b6f --- /dev/null +++ b/mobsfscan/rules/semgrep/kotlin/webview.yaml @@ -0,0 +1,147 @@ +rules: + - id: android_kotlin_webview + patterns: + - pattern-either: + - pattern: | + $W.addJavascriptInterface(...) + - pattern: | + addJavascriptInterface(...) + message: >- + Insecure WebView Implementation. Execution of user controlled code in + WebView is a critical Security Hole. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-749 + owasp-mobile: m1 + masvs: platform-7 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05h-Testing-Platform-Interaction.md#testing-javascript-execution-in-webviews-mstg-platform-5 + - id: android_kotlin_webview_allow_file_from_url + patterns: + - pattern-either: + - pattern: | + $S.setAllowFileAccessFromFileURLs(true) + - pattern: | + $S.setAllowUniversalAccessFromFileURLs(true) + - pattern: | + $S.allowFileAccessFromFileURLs = true + - pattern: | + $S.allowUniversalAccessFromFileURLs = true + message: >- + Ensure that user controlled URLs never reaches the Webview. Enabling file access + from URLs in WebView can leak sensitive information from the file system. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-200 + owasp-mobile: m1 + masvs: platform-7 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05h-Testing-Platform-Interaction.md#static-analysis-6 + - id: android_kotlin_webview_debug + patterns: + - pattern-either: + - pattern: | + WebView.setWebContentsDebuggingEnabled(true) + - pattern: | + $W.setWebContentsDebuggingEnabled(true) + message: Remote WebView debugging is enabled. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-489 + owasp-mobile: m1 + masvs: resilience-2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05j-Testing-Resiliency-Against-Reverse-Engineering.md#testing-anti-debugging-detection-mstg-resilience-2 + - id: android_kotlin_webview_ignore_ssl + patterns: + - pattern-either: + - pattern: | + override fun onReceivedSslError($W: WebView, $H: SslErrorHandler, $E: SslError) { + ... + $H.proceed() + ... + } + - pattern: | + fun onReceivedSslError($W: WebView, $H: SslErrorHandler, $E: SslError) { + ... + $H.proceed() + ... + } + message: >- + Insecure WebView Implementation. WebView ignores SSL Certificate errors and + accept any SSL Certificate. This application is vulnerable to MITM attacks + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-295 + owasp-mobile: m3 + masvs: network-3 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05g-Testing-Network-Communication.md#webview-server-certificate-verification + - id: android_kotlin_webview_mixed_content + patterns: + - pattern-either: + - pattern: | + $S.setMixedContentMode($X.MIXED_CONTENT_ALWAYS_ALLOW) + - pattern: | + $S.mixedContentMode = $X.MIXED_CONTENT_ALWAYS_ALLOW + message: >- + Insecure WebView Implementation. WebView is configured with + MIXED_CONTENT_ALWAYS_ALLOW, allowing a page loaded over HTTPS to load + content from insecure HTTP origins. This exposes the application to + man-in-the-middle content injection. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-319 + owasp-mobile: m3 + masvs: network-1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05g-Testing-Network-Communication.md + - id: android_kotlin_webview_external + patterns: + - pattern-either: + - pattern: | + $W.loadUrl($E.getExternalStorageDirectory().$M()) + - pattern: | + $W.loadUrl(<... getExternalStorageDirectory() ...>) + - pattern-regex: '\.loadUrl\([^)\n]{0,200}getExternalStorageDirectory\(' + message: >- + WebView load files from external storage. Files in external storage can be + modified by any application. + languages: + - kotlin + severity: ERROR + metadata: + cwe: cwe-749 + owasp-mobile: m1 + masvs: platform-6 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05h-Testing-Platform-Interaction.md#static-analysis-7 + - id: android_kotlin_webview_set_allow_file_access + patterns: + - pattern-either: + - pattern: | + $S.setAllowFileAccess(true) + - pattern: | + $S.allowFileAccess = true + message: >- + WebView File System Access is enabled. An attacker able to inject script into a WebView, could exploit the opportunity to access local resources. + languages: + - kotlin + severity: WARNING + metadata: + cwe: cwe-73 + owasp-mobile: m7 + masvs: platform-6 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05h-Testing-Platform-Interaction.md/#testing-webview-protocol-handlers-mstg-platform-6 + diff --git a/mobsfscan/rules/semgrep/swift/auth.yaml b/mobsfscan/rules/semgrep/swift/auth.yaml new file mode 100644 index 0000000..a81e791 --- /dev/null +++ b/mobsfscan/rules/semgrep/swift/auth.yaml @@ -0,0 +1,59 @@ +rules: + - id: ios_biometric_bool + patterns: + - pattern: | + $C.evaluatePolicy(...) + - pattern-regex: '\.deviceOwnerAuthentication\b' + message: >- + Biometric authentication should be hardware and keychain backed, local authentication returns a boolean that can be bypassed by runtime instrumentation tools like Frida. This is not applicable if authentication data in keychain is protected with a biometric only access control. + languages: + - swift + severity: WARNING + metadata: + cwe: cwe-303 + masvs: auth-8 + owasp-mobile: m1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06f-Testing-Local-Authentication.md#local-authentication-framework + - id: ios_biometric_acl + patterns: + - pattern-regex: 'SecAccessControlCreateWithFlags\([^)\n]*\.(biometryAny|userPresence|touchIDAny)\b' + message: >- + Weak biometric ACL flag is associated with a key stored in Keychain. With `.biometryAny/.userPresence/.touchIDAny` flag, an attacker with the ability to add a biometry to the device can authenticate as the user. Use `.biometryCurrentSet/.touchIDCurrentSet` instead. + languages: + - swift + severity: ERROR + metadata: + cwe: cwe-305 + masvs: auth-8 + owasp-mobile: m1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06f-Testing-Local-Authentication.md#local-authentication-framework + - id: ios_keychain_weak_acl_device_passcode + patterns: + - pattern-regex: 'SecAccessControlCreateWithFlags\([^)\n]*\.devicePasscode\b' + message: >- + A key stored in the Keychain is not making use of stronger biometric backed ACL. Use `.biometryCurrentSet` instead. + languages: + - swift + severity: WARNING + metadata: + cwe: cwe-305 + masvs: auth-8 + owasp-mobile: m1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06f-Testing-Local-Authentication.md + - id: ios_keychain_weak_accessibility_value + patterns: + - pattern-regex: '\bkSecAttrAccessibleAlways\b|\bkSecAttrAccessibleAfterFirstUnlock\b' + message: >- + A key stored in the Keychain is using a weak accessibility value. Use stronger ACLs like `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly/kSecAttrAccessibleWhenUnlocked/kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`. + languages: + - swift + severity: WARNING + metadata: + cwe: cwe-305 + masvs: auth-8 + owasp-mobile: m1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06f-Testing-Local-Authentication.md diff --git a/mobsfscan/rules/semgrep/swift/crypto.yaml b/mobsfscan/rules/semgrep/swift/crypto.yaml new file mode 100644 index 0000000..db36c7f --- /dev/null +++ b/mobsfscan/rules/semgrep/swift/crypto.yaml @@ -0,0 +1,66 @@ +rules: + - id: ios_sha1_collision + patterns: + - pattern-either: + - pattern-regex: '(?i)\bSHA1\s*\(' + - pattern-regex: 'CC_SHA1\s*\(' + message: SHA1 is a weak hash known to have hash collisions. + languages: + - swift + severity: WARNING + metadata: + cwe: cwe-327 + masvs: crypto-4 + owasp-mobile: m5 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 + - id: ios_weak_hash + patterns: + - pattern-either: + - pattern-regex: '(?i)\bMD2\s*\(' + - pattern-regex: 'CC_MD2\s*\(' + - pattern-regex: '(?i)\bMD4\s*\(' + - pattern-regex: 'CC_MD4\s*\(' + - pattern-regex: '(?i)\bMD5\s*\(' + - pattern-regex: 'CC_MD5\s*\(' + - pattern-regex: '(?i)\bMD6\s*\(' + - pattern-regex: 'CC_MD6\s*\(' + message: Weak Hash algorithm used. The hash algorithm is known to have hash collisions. + languages: + - swift + severity: WARNING + metadata: + cwe: cwe-327 + masvs: crypto-4 + owasp-mobile: m5 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#identifying-insecure-andor-deprecated-cryptographic-algorithms-mstg-crypto-4 + - id: ios_insecure_random_no_generator + patterns: + - pattern-either: + - pattern: | + Int.random(...) + - pattern: | + Bool.random(...) + - pattern: | + Float.random(...) + - pattern: | + Double.random(...) + - pattern: | + arc4random() + - pattern: | + arc4random_uniform(...) + - pattern: | + SystemRandomNumberGenerator() + - pattern-regex: '\brand\s*\(' + - pattern-regex: '\brandom\s*\(' + message: The App uses an insecure Random Number Generator. + languages: + - swift + severity: WARNING + metadata: + cwe: cwe-330 + owasp-mobile: m5 + masvs: crypto-6 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04g-Testing-Cryptography.md#weak-random-number-generators diff --git a/mobsfscan/rules/semgrep/swift/logging.yaml b/mobsfscan/rules/semgrep/swift/logging.yaml new file mode 100644 index 0000000..a47eb86 --- /dev/null +++ b/mobsfscan/rules/semgrep/swift/logging.yaml @@ -0,0 +1,23 @@ +rules: + - id: ios_log + patterns: + - pattern-either: + - pattern: | + NSLog(...) + - pattern: | + os_log(...) + - pattern: | + OSLog(...) + - pattern: | + os_signpost(...) + message: >- + The App logs information to the system console. Sensitive information should never be logged. + languages: + - swift + severity: INFO + metadata: + cwe: cwe-532 + masvs: storage-3 + owasp-mobile: m1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06i-Testing-Code-Quality-and-Build-Settings.md#finding-debugging-code-and-verbose-error-logging-mstg-code-4 diff --git a/mobsfscan/rules/semgrep/swift/network.yaml b/mobsfscan/rules/semgrep/swift/network.yaml new file mode 100644 index 0000000..c16276b --- /dev/null +++ b/mobsfscan/rules/semgrep/swift/network.yaml @@ -0,0 +1,55 @@ +rules: + - id: ios_tls3_not_used + patterns: + - pattern-regex: 'TLSMinimumSupportedProtocolVersion\s*=\s*(?:tls_protocol_version_t\.)?\.?TLSv1[01]\b' + message: The app uses TLS 1.0 or TLS 1.1. TLS 1.3 should be used instead. + languages: + - swift + severity: ERROR + metadata: + cwe: cwe-757 + masvs: network-2 + owasp-mobile: m3 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04f-Testing-Network-Communication.md#verifying-data-encryption-on-the-network-mstg-network-1-and-mstg-network-2 + - id: ios_tls12_used + patterns: + - pattern-regex: 'TLSMinimumSupportedProtocolVersion\s*=\s*(?:tls_protocol_version_t\.)?\.?TLSv12\b' + message: This app uses TLS 1.2. TLS 1.3 should be used instead. + languages: + - swift + severity: WARNING + metadata: + cwe: cwe-757 + masvs: network-2 + owasp-mobile: m3 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04f-Testing-Network-Communication.md#verifying-data-encryption-on-the-network-mstg-network-1-and-mstg-network-2 + - id: ios_dtls1_used + patterns: + - pattern-regex: 'TLSMinimumSupportedProtocolVersion\s*=\s*(?:tls_protocol_version_t\.)?\.?DTLSv10\b' + message: DTLS 1.2 should be used. Detected old version - DTLS 1.0. + languages: + - swift + severity: WARNING + metadata: + cwe: cwe-757 + masvs: network-2 + owasp-mobile: m3 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04f-Testing-Network-Communication.md#verifying-data-encryption-on-the-network-mstg-network-1-and-mstg-network-2 + - id: ios_depr_tls_min + patterns: + - pattern-regex: '\.tlsMinimumSupportedProtocol\b' + message: >- + Use of deprecated property tlsMinimumSupportedProtocol. To avoid potential + security risks, use tlsMinimumSupportedProtocolVersion + languages: + - swift + severity: WARNING + metadata: + cwe: cwe-757 + masvs: network-2 + owasp-mobile: m3 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x04f-Testing-Network-Communication.md#verifying-data-encryption-on-the-network-mstg-network-1-and-mstg-network-2 diff --git a/mobsfscan/rules/semgrep/swift/secrets.yaml b/mobsfscan/rules/semgrep/swift/secrets.yaml new file mode 100644 index 0000000..cfe16de --- /dev/null +++ b/mobsfscan/rules/semgrep/swift/secrets.yaml @@ -0,0 +1,33 @@ +rules: + - id: ios_hardcoded_secret + patterns: + - pattern-either: + - pattern: | + let $X = "..." + - pattern: | + var $X = "..." + - pattern: | + private let $X = "..." + - pattern: | + private var $X = "..." + - pattern: | + static let $X = "..." + - pattern: | + private static let $X = "..." + - pattern-not: | + let $X = "" + - metavariable-regex: + metavariable: $X + regex: '(?i)^(?:password|pass|username|secret|key|(?:api|secret|private|access|encryption|auth)_?key)$' + message: >- + Files may contain hardcoded sensitive information like usernames, + passwords, keys etc. + languages: + - swift + severity: WARNING + metadata: + cwe: cwe-312 + masvs: storage-14 + owasp-mobile: m9 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05d-Testing-Data-Storage.md#checking-memory-for-sensitive-data-mstg-storage-10 diff --git a/mobsfscan/rules/semgrep/swift/storage.yaml b/mobsfscan/rules/semgrep/swift/storage.yaml new file mode 100644 index 0000000..6c48eab --- /dev/null +++ b/mobsfscan/rules/semgrep/swift/storage.yaml @@ -0,0 +1,29 @@ +rules: + - id: ios_file_no_special + patterns: + - pattern-regex: '(?i)\.noFileProtection\b' + message: The file has no special protections associated with it. + languages: + - swift + severity: ERROR + metadata: + cwe: cwe-311 + masvs: storage-1 + owasp-mobile: m2 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06d-Testing-Data-Storage.md#ios-data-storage + - id: ios_general_paste + patterns: + - pattern: | + UIPasteboard.generalPasteboard + message: >- + Usage of generalPasteboard should be avoided. A malicious app can monitor the pasteboard in the background in iOS versions below 9. + languages: + - swift + severity: INFO + metadata: + cwe: cwe-919 + masvs: platform-4 + owasp-mobile: m1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06h-Testing-Platform-Interaction.md#uipasteboard diff --git a/mobsfscan/rules/semgrep/swift/webview.yaml b/mobsfscan/rules/semgrep/swift/webview.yaml new file mode 100644 index 0000000..9f65ec6 --- /dev/null +++ b/mobsfscan/rules/semgrep/swift/webview.yaml @@ -0,0 +1,32 @@ +rules: + - id: ios_load_html_string + patterns: + - pattern-either: + - pattern: | + $W.loadHTMLString(...) + - pattern: | + loadHTMLString(...) + message: User input in "loadHTMLString" will result in JavaScript Injection. + languages: + - swift + severity: INFO + metadata: + cwe: cwe-95 + masvs: platform-5 + owasp-mobile: m7 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06h-Testing-Platform-Interaction.md#testing-webview-protocol-handlers-mstg-platform-6 + - id: ios_uiwebview + patterns: + - pattern-regex: '\bUIWebView\b' + message: >- + This app uses UIWebView. For security reasons, It is recommended to use WKWebView instead. + languages: + - swift + severity: INFO + metadata: + cwe: cwe-919 + masvs: platform-5 + owasp-mobile: m1 + reference: >- + https://github.com/MobSF/owasp-mstg/blob/master/Document/0x06h-Testing-Platform-Interaction.md#testing-ios-webviews-mstg-platform-5 diff --git a/mobsfscan/settings.py b/mobsfscan/settings.py index dd405d4..33007cc 100644 --- a/mobsfscan/settings.py +++ b/mobsfscan/settings.py @@ -8,9 +8,6 @@ SGREP_RULES_DIR = ( BASE_DIR / 'rules' / 'semgrep' ) -ANDROID_RULES_DIR = ( - BASE_DIR / 'rules' / 'patterns' / 'android' -) IOS_RULES_DIR = ( BASE_DIR / 'rules' / 'patterns' / 'ios' ) diff --git a/mobsfscan/utils.py b/mobsfscan/utils.py index e541000..1aefa3f 100644 --- a/mobsfscan/utils.py +++ b/mobsfscan/utils.py @@ -14,6 +14,19 @@ logger = init_logger(__name__) +def report_path(path): + """Prefer cwd-relative POSIX paths in findings (matches Semgrep/source). + + Absolute paths cause duplicate findings in ASOC/VM tools when the same + project is scanned from different working directories (#109). + """ + p = Path(path) + try: + return p.resolve().relative_to(Path.cwd().resolve()).as_posix() + except (ValueError, OSError): + return p.as_posix() + + def filter_none(user_list): """Filter and remove None values from user supplied config.""" if not user_list: @@ -21,6 +34,31 @@ def filter_none(user_list): return list(filter(lambda item: item is not None, user_list)) +VALID_SEVERITIES = {'INFO', 'WARNING', 'ERROR'} + + +def normalize_severity_overrides(raw): + """Parse severity-overrides map; return {rule_id: SEVERITY}.""" + if not raw or not isinstance(raw, dict): + return {} + overrides = {} + for rule_id, severity in raw.items(): + if rule_id is None or severity is None: + continue + rid = str(rule_id).strip() + sev = str(severity).strip().upper() + if not rid: + continue + if sev not in VALID_SEVERITIES: + logger.warning( + 'Invalid severity `%s` for rule `%s` in ' + 'severity-overrides. Use INFO, WARNING, or ERROR.', + severity, rid) + continue + overrides[rid] = sev + return overrides + + def get_config(base_path, config_file): options = { 'ignore_filenames': config.IGNORE_FILENAMES, @@ -28,6 +66,7 @@ def get_config(base_path, config_file): 'ignore_paths': config.IGNORE_PATHS, 'ignore_rules': set(), 'severity_filter': config.SEVERITY_FILTER, + 'severity_overrides': {}, } if config_file: cfile = Path(config_file) @@ -43,6 +82,8 @@ def get_config(base_path, config_file): usr_igonre_paths = filter_none(root.get('ignore-paths')) usr_ignore_rules = filter_none(root.get('ignore-rules')) usr_severity_filter = filter_none(root.get('severity-filter')) + usr_severity_overrides = normalize_severity_overrides( + root.get('severity-overrides')) if usr_ignore_files: options['ignore_filenames'].update(usr_ignore_files) if usr_igonre_paths: @@ -51,6 +92,8 @@ def get_config(base_path, config_file): options['ignore_rules'].update(usr_ignore_rules) if usr_severity_filter: options['severity_filter'] = usr_severity_filter + if usr_severity_overrides: + options['severity_overrides'] = usr_severity_overrides return options @@ -64,9 +107,19 @@ def validate_config(extras, options): root = extras[0] valid = True for key, value in root.items(): - if key.replace('-', '_') not in options.keys(): + opt_key = key.replace('-', '_') + if opt_key not in options.keys(): valid = False logger.warning('The config `%s` is not supported.', key) + continue + if opt_key == 'severity_overrides': + if not isinstance(value, dict): + valid = False + logger.warning( + 'The value `%s` for the config `%s` is invalid.' + ' Only a mapping of rule_id: severity is supported.', + value, key) + continue if not isinstance(value, list): valid = False logger.warning('The value `%s` for the config `%s` is invalid.' @@ -89,31 +142,52 @@ def read_yaml(file_obj, text=False): return None -def get_best_practices(extension): - """Get best practices of an extension.""" +def get_best_practices(extensions): + """Get best practices for one or more extensions. + + Best-practice rules match control *presence*. MobSFScan.missing_controls() + inverts them: delete when present, report when missing across the scan. + """ + if isinstance(extensions, str): + extensions = [extensions] ids = set() all_rules = {} - if extension == '.java': - for yml in config.BEST_PRACTICES_DIR.rglob('*.yaml'): - rules = read_yaml(yml) - for rule in rules['rules']: + for extension in extensions: + if extension == '.java': + java_dir = config.BEST_PRACTICES_DIR / 'java' + for yml in java_dir.rglob('*.yaml'): + rules = read_yaml(yml) + if not rules or 'rules' not in rules: + continue + for rule in rules['rules']: + all_rules[rule['id']] = rule + ids.add(rule['id']) + elif extension == '.kt': + kt_dir = config.BEST_PRACTICES_DIR / 'kotlin' + for yml in kt_dir.rglob('*.yaml'): + rules = read_yaml(yml) + if not rules or 'rules' not in rules: + continue + for rule in rules['rules']: + all_rules[rule['id']] = rule + ids.add(rule['id']) + elif extension == '.swift': + swift_dir = config.BEST_PRACTICES_DIR / 'swift' + for yml in swift_dir.rglob('*.yaml'): + rules = read_yaml(yml) + if not rules or 'rules' not in rules: + continue + for rule in rules['rules']: + all_rules[rule['id']] = rule + ids.add(rule['id']) + elif extension == '.m': + bp = config.IOS_RULES_DIR / 'objectivec' / 'best_practices.yaml' + rules = read_yaml(bp) + if not rules: + continue + for rule in rules: all_rules[rule['id']] = rule ids.add(rule['id']) - elif extension in ['.kt', '.m', '.swift']: - if extension == '.kt': - os_dir = config.ANDROID_RULES_DIR - lang = 'kotlin' - elif extension == '.m': - os_dir = config.IOS_RULES_DIR - lang = 'objectivec' - elif extension == '.swift': - os_dir = config.IOS_RULES_DIR - lang = 'swift' - kt = os_dir / lang / 'best_practices.yaml' - rules = read_yaml(kt) - for rule in rules: - all_rules[rule['id']] = rule - ids.add(rule['id']) return ids, all_rules diff --git a/requirements.txt b/requirements.txt index 64a8e5c..98561fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,12 +20,12 @@ httpcore==1.0.9 httpx==0.28.1 httpx-sse==0.4.3 idna==3.18 -importlib-metadata==8.7.1 +importlib_metadata==8.7.1 jschema-to-python==1.2.3 jsonpickle==4.1.2 jsonschema==4.25.1 jsonschema-specifications==2025.9.1 -libsast==3.1.7 +libsast==3.1.8 markdown-it-py==4.2.0 mcp==1.23.3 mdurl==0.1.2 @@ -45,13 +45,13 @@ peewee==3.19.0 protobuf==6.33.6 pycparser==3.0 pydantic==2.13.4 -pydantic-core==2.46.4 pydantic-settings==2.15.0 -pygments==2.20.0 -pyjwt==2.13.0 +pydantic_core==2.46.4 +Pygments==2.20.0 +PyJWT==2.13.0 python-dotenv==1.2.2 python-multipart==0.0.32 -pyyaml==6.0.3 +PyYAML==6.0.3 referencing==0.37.0 requests==2.34.2 rich==15.0.0 @@ -66,8 +66,8 @@ sse-starlette==3.4.8 starlette==1.6.0 tabulate==0.10.0 tomli==2.4.1 -typing-extensions==4.16.0 typing-inspection==0.4.2 +typing_extensions==4.16.0 urllib3==2.7.0 uvicorn==0.52.1 wcmatch==8.5.2 diff --git a/setup.py b/setup.py index 037bbba..a3ea97d 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_version(rel_path): author='Ajin Abraham', author_email='ajin25@gmail.com', classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', ('License :: OSI Approved :: GNU Lesser ' 'General Public License v3 or later (LGPLv3+)'), @@ -55,7 +55,7 @@ def get_version(rel_path): long_description_content_type='text/markdown', install_requires=[ 'colorama>=0.4.5', - 'libsast>=3.1.7', + 'libsast>=3.1.8', 'semgrep==1.172.0', 'sarif-om>=1.0.4', 'jschema-to-python>=1.2.3', diff --git a/tests/assets/rules/semgrep/best_practices/android_safetynetapi.java b/tests/assets/rules/semgrep/best_practices/java/android_safetynetapi.java similarity index 100% rename from tests/assets/rules/semgrep/best_practices/android_safetynetapi.java rename to tests/assets/rules/semgrep/best_practices/java/android_safetynetapi.java diff --git a/tests/assets/rules/semgrep/best_practices/flag_secure.java b/tests/assets/rules/semgrep/best_practices/java/flag_secure.java similarity index 100% rename from tests/assets/rules/semgrep/best_practices/flag_secure.java rename to tests/assets/rules/semgrep/best_practices/java/flag_secure.java diff --git a/tests/assets/rules/semgrep/best_practices/root_detection.java b/tests/assets/rules/semgrep/best_practices/java/root_detection.java similarity index 100% rename from tests/assets/rules/semgrep/best_practices/root_detection.java rename to tests/assets/rules/semgrep/best_practices/java/root_detection.java diff --git a/tests/assets/rules/semgrep/best_practices/tapjacking.java b/tests/assets/rules/semgrep/best_practices/java/tapjacking.java similarity index 100% rename from tests/assets/rules/semgrep/best_practices/tapjacking.java rename to tests/assets/rules/semgrep/best_practices/java/tapjacking.java diff --git a/tests/assets/rules/semgrep/best_practices/tls_certificate_transparency.java b/tests/assets/rules/semgrep/best_practices/java/tls_certificate_transparency.java similarity index 100% rename from tests/assets/rules/semgrep/best_practices/tls_certificate_transparency.java rename to tests/assets/rules/semgrep/best_practices/java/tls_certificate_transparency.java diff --git a/tests/assets/rules/semgrep/best_practices/tls_pinning.java b/tests/assets/rules/semgrep/best_practices/java/tls_pinning.java similarity index 100% rename from tests/assets/rules/semgrep/best_practices/tls_pinning.java rename to tests/assets/rules/semgrep/best_practices/java/tls_pinning.java diff --git a/tests/assets/rules/semgrep/best_practices/kotlin/android_safetynet.kt b/tests/assets/rules/semgrep/best_practices/kotlin/android_safetynet.kt new file mode 100644 index 0000000..7bcd235 --- /dev/null +++ b/tests/assets/rules/semgrep/best_practices/kotlin/android_safetynet.kt @@ -0,0 +1,7 @@ +fun attest(context: android.content.Context) { + // ruleid:android_safetynet + SafetyNet.getClient(context) +} + +// ruleid:android_safetynet +val api = "com.google.android.gms.safetynet.SafetyNetApi" diff --git a/tests/assets/rules/semgrep/best_practices/kotlin/flag_secure.kt b/tests/assets/rules/semgrep/best_practices/kotlin/flag_secure.kt new file mode 100644 index 0000000..11d50c2 --- /dev/null +++ b/tests/assets/rules/semgrep/best_practices/kotlin/flag_secure.kt @@ -0,0 +1,10 @@ + +fun protect(window: android.view.Window) { + // ruleid:android_prevent_screenshot + window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE, + ) + // ruleid:android_prevent_screenshot + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) +} diff --git a/tests/assets/rules/semgrep/best_practices/kotlin/root_detection.kt b/tests/assets/rules/semgrep/best_practices/kotlin/root_detection.kt new file mode 100644 index 0000000..427d796 --- /dev/null +++ b/tests/assets/rules/semgrep/best_practices/kotlin/root_detection.kt @@ -0,0 +1,11 @@ + +fun check(device: Device) { + // ruleid:android_root_detection + device.isRooted() + // ruleid:android_root_detection + device.isDeviceRooted() + // ruleid:android_root_detection + RootTools.isAccessGiven() + // ruleid:android_root_detection + buildTags.contains("test-keys") +} diff --git a/tests/assets/rules/semgrep/best_practices/kotlin/tapjacking.kt b/tests/assets/rules/semgrep/best_practices/kotlin/tapjacking.kt new file mode 100644 index 0000000..b004e41 --- /dev/null +++ b/tests/assets/rules/semgrep/best_practices/kotlin/tapjacking.kt @@ -0,0 +1,5 @@ + +fun protect(view: android.view.View) { + // ruleid:android_tapjacking + view.setFilterTouchesWhenObscured(true) +} diff --git a/tests/assets/rules/semgrep/best_practices/kotlin/tls_certificate_transparency.kt b/tests/assets/rules/semgrep/best_practices/kotlin/tls_certificate_transparency.kt new file mode 100644 index 0000000..27dd9f5 --- /dev/null +++ b/tests/assets/rules/semgrep/best_practices/kotlin/tls_certificate_transparency.kt @@ -0,0 +1,7 @@ + +fun enable() { + // ruleid:android_certificate_transparency + CTHostnameVerifierBuilder(hostnameVerifier) + // ruleid:android_certificate_transparency + CTInterceptorBuilder() +} diff --git a/tests/assets/rules/semgrep/best_practices/kotlin/tls_pinning.kt b/tests/assets/rules/semgrep/best_practices/kotlin/tls_pinning.kt new file mode 100644 index 0000000..c060b6e --- /dev/null +++ b/tests/assets/rules/semgrep/best_practices/kotlin/tls_pinning.kt @@ -0,0 +1,7 @@ + +fun pin() { + // ruleid:android_ssl_pinning + CertificatePinner.Builder() + .add("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") + .build() +} diff --git a/tests/assets/rules/semgrep/best_practices/swift/jailbreak.swift b/tests/assets/rules/semgrep/best_practices/swift/jailbreak.swift new file mode 100644 index 0000000..911243f --- /dev/null +++ b/tests/assets/rules/semgrep/best_practices/swift/jailbreak.swift @@ -0,0 +1,5 @@ + +func detect() { + // ruleid:ios_jailbreak_detect + let path = "/Applications/Cydia.app" +} diff --git a/tests/assets/rules/semgrep/best_practices/swift/keyboard.swift b/tests/assets/rules/semgrep/best_practices/swift/keyboard.swift new file mode 100644 index 0000000..c618b4a --- /dev/null +++ b/tests/assets/rules/semgrep/best_practices/swift/keyboard.swift @@ -0,0 +1,9 @@ + +func disable(field: UITextField) { + // ruleid:ios_keyboard_cache + field.autocorrectionType = .no +} +func blockKeyboard() { + // ruleid:ios_custom_keyboard_disabled + if extensionPointIdentifier == UIExtensionPointIdentifier.keyboard {} +} diff --git a/tests/assets/rules/semgrep/best_practices/swift/resilience.swift b/tests/assets/rules/semgrep/best_practices/swift/resilience.swift new file mode 100644 index 0000000..af436f3 --- /dev/null +++ b/tests/assets/rules/semgrep/best_practices/swift/resilience.swift @@ -0,0 +1,9 @@ + +func reverse() { + // ruleid:ios_detect_reversing + let markers = ["FridaGadget", "cynject", "libcycript", "/usr/sbin/frida-server"] +} +func pin() { + // ruleid:ios_cert_pinning + TrustKit.initSharedInstance(with: [:]) +} diff --git a/tests/assets/rules/semgrep/java/android/biometric_crypto.java b/tests/assets/rules/semgrep/java/android/biometric_crypto.java new file mode 100644 index 0000000..ef3cdc3 --- /dev/null +++ b/tests/assets/rules/semgrep/java/android/biometric_crypto.java @@ -0,0 +1,18 @@ +class UnsafeCallback extends BiometricPrompt.AuthenticationCallback { + // ruleid:android_biometric_without_crypto + @Override + public void onAuthenticationSucceeded( + BiometricPrompt.AuthenticationResult result) { + unlockAccount(); + } +} + +class SafeCallback extends BiometricPrompt.AuthenticationCallback { + // ok:android_biometric_without_crypto + @Override + public void onAuthenticationSucceeded( + BiometricPrompt.AuthenticationResult result) { + BiometricPrompt.CryptoObject crypto = result.getCryptoObject(); + decryptAccount(crypto.getCipher()); + } +} diff --git a/tests/assets/rules/semgrep/android/hidden_ui.java b/tests/assets/rules/semgrep/java/android/hidden_ui.java similarity index 100% rename from tests/assets/rules/semgrep/android/hidden_ui.java rename to tests/assets/rules/semgrep/java/android/hidden_ui.java diff --git a/tests/assets/rules/semgrep/android/logging.java b/tests/assets/rules/semgrep/java/android/logging.java similarity index 100% rename from tests/assets/rules/semgrep/android/logging.java rename to tests/assets/rules/semgrep/java/android/logging.java diff --git a/tests/assets/rules/semgrep/android/secrets.java b/tests/assets/rules/semgrep/java/android/secrets.java similarity index 100% rename from tests/assets/rules/semgrep/android/secrets.java rename to tests/assets/rules/semgrep/java/android/secrets.java diff --git a/tests/assets/rules/semgrep/java/android/sensitive_input.java b/tests/assets/rules/semgrep/java/android/sensitive_input.java new file mode 100644 index 0000000..cd2279f --- /dev/null +++ b/tests/assets/rules/semgrep/java/android/sensitive_input.java @@ -0,0 +1,14 @@ +void configureFields(EditText passwordField, EditText emailField) { + // ruleid:android_sensitive_input_keyboard_cache + passwordField.setInputType(InputType.TYPE_CLASS_TEXT); + + // ok:android_sensitive_input_keyboard_cache + passwordField.setInputType( + InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_VARIATION_PASSWORD); + + // ok:android_sensitive_input_keyboard_cache + emailField.setInputType( + InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); +} diff --git a/tests/assets/rules/semgrep/java/android/sensitive_notification.java b/tests/assets/rules/semgrep/java/android/sensitive_notification.java new file mode 100644 index 0000000..7e19c8e --- /dev/null +++ b/tests/assets/rules/semgrep/java/android/sensitive_notification.java @@ -0,0 +1,13 @@ +void buildNotifications( + NotificationCompat.Builder builder, + String oneTimePassword, + String accountName) { + // ruleid:android_sensitive_notification + builder.setContentText(oneTimePassword); + + // ok:android_sensitive_notification + builder.setContentTitle(accountName); + + // ok:android_sensitive_notification + builder.setContentText("Open the app to continue"); +} diff --git a/tests/assets/rules/semgrep/android/word_readable_writable.java b/tests/assets/rules/semgrep/java/android/word_readable_writable.java similarity index 100% rename from tests/assets/rules/semgrep/android/word_readable_writable.java rename to tests/assets/rules/semgrep/java/android/word_readable_writable.java diff --git a/tests/assets/rules/semgrep/crypto/aes_ecb.java b/tests/assets/rules/semgrep/java/crypto/aes_ecb.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/aes_ecb.java rename to tests/assets/rules/semgrep/java/crypto/aes_ecb.java diff --git a/tests/assets/rules/semgrep/crypto/aes_encryption_keys.java b/tests/assets/rules/semgrep/java/crypto/aes_encryption_keys.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/aes_encryption_keys.java rename to tests/assets/rules/semgrep/java/crypto/aes_encryption_keys.java diff --git a/tests/assets/rules/semgrep/crypto/cbc_padding_oracle.java b/tests/assets/rules/semgrep/java/crypto/cbc_padding_oracle.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/cbc_padding_oracle.java rename to tests/assets/rules/semgrep/java/crypto/cbc_padding_oracle.java diff --git a/tests/assets/rules/semgrep/crypto/cbc_static_iv.java b/tests/assets/rules/semgrep/java/crypto/cbc_static_iv.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/cbc_static_iv.java rename to tests/assets/rules/semgrep/java/crypto/cbc_static_iv.java diff --git a/tests/assets/rules/semgrep/java/crypto/custom_xor_crypto.java b/tests/assets/rules/semgrep/java/crypto/custom_xor_crypto.java new file mode 100644 index 0000000..acdb47b --- /dev/null +++ b/tests/assets/rules/semgrep/java/crypto/custom_xor_crypto.java @@ -0,0 +1,18 @@ +byte[] encrypt(byte[] plaintext, byte key) { + byte[] output = new byte[plaintext.length]; + for (int i = 0; i < plaintext.length; i++) { + // ruleid:android_custom_xor_crypto + output[i] = (byte) (plaintext[i] ^ key); + } + return output; +} + +int toggleFlag(int flags, int mask) { + // ok:android_custom_xor_crypto + return flags ^ mask; +} + +byte[] encrypt(byte[] plaintext, SecretKey key) { + // ok:android_custom_xor_crypto + return standardCipher.doFinal(plaintext); +} diff --git a/tests/assets/rules/semgrep/crypto/insecure_random.java b/tests/assets/rules/semgrep/java/crypto/insecure_random.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/insecure_random.java rename to tests/assets/rules/semgrep/java/crypto/insecure_random.java diff --git a/tests/assets/rules/semgrep/crypto/insecure_ssl_v3.java b/tests/assets/rules/semgrep/java/crypto/insecure_ssl_v3.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/insecure_ssl_v3.java rename to tests/assets/rules/semgrep/java/crypto/insecure_ssl_v3.java diff --git a/tests/assets/rules/semgrep/crypto/rsa_no_oeap.java b/tests/assets/rules/semgrep/java/crypto/rsa_no_oeap.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/rsa_no_oeap.java rename to tests/assets/rules/semgrep/java/crypto/rsa_no_oeap.java diff --git a/tests/assets/rules/semgrep/crypto/sha1_hash.java b/tests/assets/rules/semgrep/java/crypto/sha1_hash.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/sha1_hash.java rename to tests/assets/rules/semgrep/java/crypto/sha1_hash.java diff --git a/tests/assets/rules/semgrep/crypto/weak_ciphers.java b/tests/assets/rules/semgrep/java/crypto/weak_ciphers.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/weak_ciphers.java rename to tests/assets/rules/semgrep/java/crypto/weak_ciphers.java diff --git a/tests/assets/rules/semgrep/crypto/weak_hashes.java b/tests/assets/rules/semgrep/java/crypto/weak_hashes.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/weak_hashes.java rename to tests/assets/rules/semgrep/java/crypto/weak_hashes.java diff --git a/tests/assets/rules/semgrep/crypto/weak_iv.java b/tests/assets/rules/semgrep/java/crypto/weak_iv.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/weak_iv.java rename to tests/assets/rules/semgrep/java/crypto/weak_iv.java diff --git a/tests/assets/rules/semgrep/crypto/weak_key_size.java b/tests/assets/rules/semgrep/java/crypto/weak_key_size.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/weak_key_size.java rename to tests/assets/rules/semgrep/java/crypto/weak_key_size.java diff --git a/tests/assets/rules/semgrep/deserialization/jackson_deserialization.java b/tests/assets/rules/semgrep/java/deserialization/jackson_deserialization.java similarity index 100% rename from tests/assets/rules/semgrep/deserialization/jackson_deserialization.java rename to tests/assets/rules/semgrep/java/deserialization/jackson_deserialization.java diff --git a/tests/assets/rules/semgrep/deserialization/object_deserialization.java b/tests/assets/rules/semgrep/java/deserialization/object_deserialization.java similarity index 100% rename from tests/assets/rules/semgrep/deserialization/object_deserialization.java rename to tests/assets/rules/semgrep/java/deserialization/object_deserialization.java diff --git a/tests/assets/rules/semgrep/injection/command_injection.java b/tests/assets/rules/semgrep/java/injection/command_injection.java similarity index 100% rename from tests/assets/rules/semgrep/injection/command_injection.java rename to tests/assets/rules/semgrep/java/injection/command_injection.java diff --git a/tests/assets/rules/semgrep/injection/command_injection_formated.java b/tests/assets/rules/semgrep/java/injection/command_injection_formated.java similarity index 100% rename from tests/assets/rules/semgrep/injection/command_injection_formated.java rename to tests/assets/rules/semgrep/java/injection/command_injection_formated.java diff --git a/tests/assets/rules/semgrep/injection/sqlite_injection.java b/tests/assets/rules/semgrep/java/injection/sqlite_injection.java similarity index 100% rename from tests/assets/rules/semgrep/injection/sqlite_injection.java rename to tests/assets/rules/semgrep/java/injection/sqlite_injection.java diff --git a/tests/assets/rules/semgrep/network/accept_self_signed.java b/tests/assets/rules/semgrep/java/network/accept_self_signed.java similarity index 100% rename from tests/assets/rules/semgrep/network/accept_self_signed.java rename to tests/assets/rules/semgrep/java/network/accept_self_signed.java diff --git a/tests/assets/rules/semgrep/network/default_http_client.tls.java b/tests/assets/rules/semgrep/java/network/default_http_client.tls.java similarity index 100% rename from tests/assets/rules/semgrep/network/default_http_client.tls.java rename to tests/assets/rules/semgrep/java/network/default_http_client.tls.java diff --git a/tests/assets/rules/semgrep/java/network/weak_tls_configuration.java b/tests/assets/rules/semgrep/java/network/weak_tls_configuration.java new file mode 100644 index 0000000..bb75501 --- /dev/null +++ b/tests/assets/rules/semgrep/java/network/weak_tls_configuration.java @@ -0,0 +1,23 @@ +// ruleid:insecure_tls_version +SSLContext.getInstance("TLSv1"); +// ruleid:insecure_tls_version +SSLContext.getInstance("TLSv1.1"); +// ok:insecure_tls_version +SSLContext.getInstance("TLSv1.2"); +// ok:insecure_tls_version +SSLContext.getInstance("TLSv1.3"); + +// ruleid:insecure_tls_version +socket.setEnabledProtocols(new String[] {"TLSv1", "TLSv1.2"}); +// ok:insecure_tls_version +socket.setEnabledProtocols(new String[] {"TLSv1.2", "TLSv1.3"}); + +// ruleid:weak_tls_cipher_suite +socket.setEnabledCipherSuites( + new String[] {"TLS_RSA_WITH_3DES_EDE_CBC_SHA"}); +// ruleid:weak_tls_cipher_suite +socket.setEnabledCipherSuites( + new String[] {"TLS_RSA_WITH_RC4_128_MD5"}); +// ok:weak_tls_cipher_suite +socket.setEnabledCipherSuites( + new String[] {"TLS_AES_128_GCM_SHA256"}); diff --git a/tests/assets/rules/semgrep/webview/webview_allow_file_from_url.java b/tests/assets/rules/semgrep/java/webview/webview_allow_file_from_url.java similarity index 100% rename from tests/assets/rules/semgrep/webview/webview_allow_file_from_url.java rename to tests/assets/rules/semgrep/java/webview/webview_allow_file_from_url.java diff --git a/tests/assets/rules/semgrep/webview/webview_debugging.java b/tests/assets/rules/semgrep/java/webview/webview_debugging.java similarity index 100% rename from tests/assets/rules/semgrep/webview/webview_debugging.java rename to tests/assets/rules/semgrep/java/webview/webview_debugging.java diff --git a/tests/assets/rules/semgrep/webview/webview_external_storage.java b/tests/assets/rules/semgrep/java/webview/webview_external_storage.java similarity index 100% rename from tests/assets/rules/semgrep/webview/webview_external_storage.java rename to tests/assets/rules/semgrep/java/webview/webview_external_storage.java diff --git a/tests/assets/rules/semgrep/webview/webview_file_access.java b/tests/assets/rules/semgrep/java/webview/webview_file_access.java similarity index 100% rename from tests/assets/rules/semgrep/webview/webview_file_access.java rename to tests/assets/rules/semgrep/java/webview/webview_file_access.java diff --git a/tests/assets/rules/semgrep/webview/webview_ignore_ssl_errors.java b/tests/assets/rules/semgrep/java/webview/webview_ignore_ssl_errors.java similarity index 100% rename from tests/assets/rules/semgrep/webview/webview_ignore_ssl_errors.java rename to tests/assets/rules/semgrep/java/webview/webview_ignore_ssl_errors.java diff --git a/tests/assets/rules/semgrep/webview/webview_javascript_interface.java b/tests/assets/rules/semgrep/java/webview/webview_javascript_interface.java similarity index 100% rename from tests/assets/rules/semgrep/webview/webview_javascript_interface.java rename to tests/assets/rules/semgrep/java/webview/webview_javascript_interface.java diff --git a/tests/assets/rules/semgrep/java/webview/webview_mixed_content.java b/tests/assets/rules/semgrep/java/webview/webview_mixed_content.java new file mode 100644 index 0000000..a716592 --- /dev/null +++ b/tests/assets/rules/semgrep/java/webview/webview_mixed_content.java @@ -0,0 +1,27 @@ +package com.example; + +import android.webkit.WebSettings; +import android.webkit.WebView; + +public class MixedContentWebView { + public void insecure(WebView webView) { + WebSettings settings = webView.getSettings(); + // ruleid:webview_mixed_content + settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + } + + public void insecureCompatAlias(WebView webView) { + // ruleid:webview_mixed_content + webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + } + + public void safeNeverAllow(WebView webView) { + // ok:webview_mixed_content + webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); + } + + public void safeCompatibility(WebView webView) { + // ok:webview_mixed_content + webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); + } +} diff --git a/tests/assets/rules/semgrep/xxe/xmldecoder_xxe.java b/tests/assets/rules/semgrep/java/xxe/xmldecoder_xxe.java similarity index 100% rename from tests/assets/rules/semgrep/xxe/xmldecoder_xxe.java rename to tests/assets/rules/semgrep/java/xxe/xmldecoder_xxe.java diff --git a/tests/assets/rules/semgrep/xxe/xmlfactory_external_entities_enabled.java b/tests/assets/rules/semgrep/java/xxe/xmlfactory_external_entities_enabled.java similarity index 100% rename from tests/assets/rules/semgrep/xxe/xmlfactory_external_entities_enabled.java rename to tests/assets/rules/semgrep/java/xxe/xmlfactory_external_entities_enabled.java diff --git a/tests/assets/rules/semgrep/xxe/xmlfactory_xxe.java b/tests/assets/rules/semgrep/java/xxe/xmlfactory_xxe.java similarity index 100% rename from tests/assets/rules/semgrep/xxe/xmlfactory_xxe.java rename to tests/assets/rules/semgrep/java/xxe/xmlfactory_xxe.java diff --git a/tests/assets/rules/semgrep/kotlin/android.kt b/tests/assets/rules/semgrep/kotlin/android.kt new file mode 100644 index 0000000..c4ad506 --- /dev/null +++ b/tests/assets/rules/semgrep/kotlin/android.kt @@ -0,0 +1,54 @@ + +fun hidden(v: android.view.View, show: Boolean) { + // ruleid:android_kotlin_hiddenui + v.visibility = View.GONE + // ruleid:android_kotlin_hiddenui + v.visibility = View.INVISIBLE + // ruleid:android_kotlin_hiddenui + v.visibility = if (show) View.GONE else View.VISIBLE + // ok:android_kotlin_hiddenui + v.visibility = View.VISIBLE +} + +fun logging() { + // ruleid:android_kotlin_logging + Log.e("t", "m") + // ruleid:android_kotlin_logging + System.out.println("x") +} + +fun secrets() { + // ruleid:android_kotlin_hardcoded + val password = "secret" + // ruleid:android_kotlin_hardcoded + val key = "abcd" + // ok:android_kotlin_hardcoded + val accountName = "alice" +} + +fun storage(ctx: android.content.Context) { + // ruleid:android_kotlin_world_readable + ctx.getSharedPreferences("a", Context.MODE_WORLD_READABLE) + // ruleid:android_kotlin_world_writable + ctx.getSharedPreferences("a", Context.MODE_WORLD_WRITEABLE) + // ruleid:android_kotlin_world_writable + ctx.openFileOutput("a", 2) + // ruleid:android_kotlin_world_read_write + ctx.openFileOutput("a", 3) +} + +fun input(passwordField: android.widget.EditText, emailField: android.widget.EditText) { + // ruleid:android_kotlin_sensitive_input_keyboard_cache + passwordField.inputType = InputType.TYPE_CLASS_TEXT + // ok:android_kotlin_sensitive_input_keyboard_cache + passwordField.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + // ok:android_kotlin_sensitive_input_keyboard_cache + emailField.inputType = InputType.TYPE_CLASS_TEXT +} + +fun notify(builder: androidx.core.app.NotificationCompat.Builder, oneTimePassword: String, accountName: String) { + // ruleid:android_kotlin_sensitive_notification + builder.setContentText(oneTimePassword) + // ok:android_kotlin_sensitive_notification + builder.setContentTitle(accountName) +} diff --git a/tests/assets/rules/semgrep/kotlin/biometric.kt b/tests/assets/rules/semgrep/kotlin/biometric.kt new file mode 100644 index 0000000..704d753 --- /dev/null +++ b/tests/assets/rules/semgrep/kotlin/biometric.kt @@ -0,0 +1,15 @@ + +class UnsafeCallback : BiometricPrompt.AuthenticationCallback() { + // ruleid:android_kotlin_biometric_without_crypto + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + unlockAccount() + } +} + +class SafeCallback : BiometricPrompt.AuthenticationCallback() { + // ok:android_kotlin_biometric_without_crypto + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + val crypto = result.cryptoObject + decryptAccount(crypto?.cipher) + } +} diff --git a/tests/assets/rules/semgrep/kotlin/crypto.kt b/tests/assets/rules/semgrep/kotlin/crypto.kt new file mode 100644 index 0000000..01e67dd --- /dev/null +++ b/tests/assets/rules/semgrep/kotlin/crypto.kt @@ -0,0 +1,84 @@ + +fun random() { + // ruleid:android_kotlin_insecure_random + val r = Random() +} + +fun ciphers() { + // ruleid:android_kotlin_aes_ecb + Cipher.getInstance("AES/ECB/NoPadding") + // ruleid:android_kotlin_aes_ecb_default + Cipher.getInstance("AES") + // ruleid:cbc_kotlin_padding_oracle + Cipher.getInstance("AES/CBC/PKCS5Padding") + // ruleid:android_kotlin_rsa_no_oaep + Cipher.getInstance("RSA/ECB/NoPadding") + // ruleid:android_kotlin_weak_ciphers + Cipher.getInstance("DES") + // ok:android_kotlin_aes_ecb + Cipher.getInstance("AES/GCM/NoPadding") +} + +fun hashes() { + // ruleid:android_kotlin_md5 + MessageDigest.getInstance("MD5") + // ruleid:android_kotlin_sha1 + MessageDigest.getInstance("SHA-1") + // ruleid:android_kotlin_weak_hash + MessageDigest.getInstance("MD4") +} + +fun iv() { + // ruleid:android_kotlin_weak_iv + val weak = byteArrayOf(0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00) +} + +// ruleid:android_kotlin_custom_xor_crypto +fun encryptXor(a: Int, b: Int): Int { + return a xor b +} + +// ok:android_kotlin_custom_xor_crypto +fun toggle(a: Int, b: Int) = a xor b + +fun hardcodedKey(cipher: Cipher) { + // ruleid:android_kotlin_aes_hardcoded_key + val secret = SecretKeySpec("hardcoded".toByteArray(), "AES") + cipher.init(Cipher.ENCRYPT_MODE, secret) + + // ok:android_kotlin_aes_hardcoded_key + val dynamic = SecretKeySpec(password.toByteArray(), "AES") + cipher.init(Cipher.ENCRYPT_MODE, dynamic) +} + +fun staticIv(strKey: String, plainText: String) { + // ruleid:android_kotlin_cbc_static_iv + val bytesIV = "foo".toByteArray() + val iv = IvParameterSpec(bytesIV) + val skeySpec = SecretKeySpec(strKey.toByteArray(), "AES") + // ruleid:cbc_kotlin_padding_oracle + val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING") + cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv) +} + +fun sslv3() { + // ruleid:android_kotlin_insecure_sslv3 + SSLContext.getInstance("SSLv3") + // ok:android_kotlin_insecure_sslv3 + SSLContext.getInstance("TLSv1.3") +} + +fun weakKeys() { + // ruleid:android_kotlin_weak_key_size + val kp = KeyPairGenerator.getInstance("RSA") + kp.initialize(512) + + // ruleid:android_kotlin_weak_key_size + val kg = KeyGenerator.getInstance("AES") + kg.init(64) + + // ok:android_kotlin_weak_key_size + val strong = KeyPairGenerator.getInstance("RSA") + strong.initialize(4096) +} + diff --git a/tests/assets/rules/semgrep/kotlin/injection.kt b/tests/assets/rules/semgrep/kotlin/injection.kt new file mode 100644 index 0000000..e9b085f --- /dev/null +++ b/tests/assets/rules/semgrep/kotlin/injection.kt @@ -0,0 +1,66 @@ + +fun sql(db: android.database.sqlite.SQLiteDatabase, q: String) { + // ruleid:android_kotlin_sql_raw_query + db.rawQuery(q, null) + // ruleid:android_kotlin_sql_raw_query + db.execSQL(q) +} + +fun jackson(mapper: com.fasterxml.jackson.databind.ObjectMapper) { + // ruleid:android_kotlin_jackson_deserialize + mapper.enableDefaultTyping() +} + +fun commandInjection(foo: String, input: String) { + // ruleid:android_kotlin_command_injection + Runtime.getRuntime().exec("ping somewhere.com" + foo) + // ok:android_kotlin_command_injection + Runtime.getRuntime().exec("ping somewhere.com") + + val runtime = Runtime.getRuntime() + // ruleid:android_kotlin_command_injection_warning + runtime.exec("/bin/sh -c some_tool" + input) + // ruleid:android_kotlin_command_injection_warning + runtime.loadLibrary(String.format("%s.dll", input)) + // ok:android_kotlin_command_injection_warning + runtime.exec("echo 'blah'") +} + +fun objectDeser(receivedFile: java.io.InputStream): Any { + // ruleid:android_kotlin_object_deserialization + val input = ObjectInputStream(receivedFile) + return input.readObject() +} + +fun xxeBad(): XMLInputFactory { + // ruleid:android_kotlin_xmlinputfactory_xxe + val xmlInputFactory = XMLInputFactory.newFactory() + return xmlInputFactory +} + +fun xxeGood(): XMLInputFactory { + val xmlInputFactory = XMLInputFactory.newFactory() + // ok:android_kotlin_xmlinputfactory_xxe + xmlInputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", false) + return xmlInputFactory +} + +fun xxeEnabled(factory: XMLInputFactory) { + // ruleid:android_kotlin_xmlinputfactory_xxe_enabled + factory.setProperty("javax.xml.stream.isSupportingExternalEntities", true) + // ok:android_kotlin_xmlinputfactory_xxe_enabled + factory.setProperty("javax.xml.stream.isSupportingExternalEntities", false) +} + +// ruleid:android_kotlin_xml_decoder_xxe +fun xmlDecoderBad(ins: java.io.InputStream): Any { + val decoder = XMLDecoder(ins) + return decoder.readObject() +} + +// ok:android_kotlin_xml_decoder_xxe +fun xmlDecoderGood(): Any { + val decoder = XMLDecoder("XML") + return decoder.readObject() +} + diff --git a/tests/assets/rules/semgrep/kotlin/network.kt b/tests/assets/rules/semgrep/kotlin/network.kt new file mode 100644 index 0000000..b2965c5 --- /dev/null +++ b/tests/assets/rules/semgrep/kotlin/network.kt @@ -0,0 +1,15 @@ + +fun ssl(socket: javax.net.ssl.SSLSocket) { + // ruleid:android_kotlin_insecure_ssl + HttpsURLConnection.setDefaultHostnameVerifier(NullHostnameVerifier()) + // ruleid:android_kotlin_insecure_tls_version + SSLContext.getInstance("TLSv1.1") + // ok:android_kotlin_insecure_tls_version + SSLContext.getInstance("TLSv1.2") + // ruleid:android_kotlin_insecure_tls_version + socket.setEnabledProtocols(arrayOf("TLSv1", "TLSv1.2")) + // ruleid:android_kotlin_weak_tls_cipher_suite + socket.setEnabledCipherSuites(arrayOf("TLS_RSA_WITH_RC4_128_MD5")) + // ok:android_kotlin_weak_tls_cipher_suite + socket.setEnabledCipherSuites(arrayOf("TLS_AES_128_GCM_SHA256")) +} diff --git a/tests/assets/rules/semgrep/kotlin/webview.kt b/tests/assets/rules/semgrep/kotlin/webview.kt new file mode 100644 index 0000000..a718d41 --- /dev/null +++ b/tests/assets/rules/semgrep/kotlin/webview.kt @@ -0,0 +1,35 @@ + +import android.webkit.WebView +import android.webkit.WebViewClient +import android.webkit.SslErrorHandler +import android.net.http.SslError + +fun web(wv: WebView) { + // ruleid:android_kotlin_webview + wv.addJavascriptInterface(Any(), "bridge") + // ruleid:android_kotlin_webview_allow_file_from_url + wv.settings.allowFileAccessFromFileURLs = true + // ruleid:android_kotlin_webview_debug + WebView.setWebContentsDebuggingEnabled(true) + // ruleid:android_kotlin_webview_mixed_content + wv.settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + // ruleid:android_kotlin_webview_external + wv.loadUrl(Environment.getExternalStorageDirectory().absolutePath) +} + +class InsecureClient : WebViewClient() { + // ruleid:android_kotlin_webview_ignore_ssl + override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { + handler.proceed() + } +} + +fun fileAccess(settings: WebSettings) { + // ruleid:android_kotlin_webview_set_allow_file_access + settings.allowFileAccess = true + // ruleid:android_kotlin_webview_set_allow_file_access + settings.setAllowFileAccess(true) + // ok:android_kotlin_webview_set_allow_file_access + settings.allowFileAccess = false +} + diff --git a/tests/assets/rules/semgrep/swift/auth.swift b/tests/assets/rules/semgrep/swift/auth.swift new file mode 100644 index 0000000..0e132bd --- /dev/null +++ b/tests/assets/rules/semgrep/swift/auth.swift @@ -0,0 +1,11 @@ + +func auth(ctx: LAContext) { + // ruleid:ios_biometric_bool + ctx.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "Unlock") { _, _ in } +} +// ruleid:ios_biometric_acl +_ = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlocked, .biometryAny, nil) +// ruleid:ios_keychain_weak_acl_device_passcode +_ = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlocked, .devicePasscode, nil) +// ruleid:ios_keychain_weak_accessibility_value +_ = kSecAttrAccessibleAlways diff --git a/tests/assets/rules/semgrep/swift/crypto.swift b/tests/assets/rules/semgrep/swift/crypto.swift new file mode 100644 index 0000000..9e2a5a1 --- /dev/null +++ b/tests/assets/rules/semgrep/swift/crypto.swift @@ -0,0 +1,13 @@ + +// ruleid:ios_sha1_collision +_ = SHA1(data) +// ruleid:ios_sha1_collision +_ = CC_SHA1(data) +// ruleid:ios_weak_hash +_ = MD5(data) +// ruleid:ios_weak_hash +_ = CC_MD5(data) +// ruleid:ios_insecure_random_no_generator +_ = Int.random(in: 0..<10) +// ruleid:ios_insecure_random_no_generator +_ = arc4random() diff --git a/tests/assets/rules/semgrep/swift/logging.swift b/tests/assets/rules/semgrep/swift/logging.swift new file mode 100644 index 0000000..6db4480 --- /dev/null +++ b/tests/assets/rules/semgrep/swift/logging.swift @@ -0,0 +1,7 @@ + +// ok:ios_log +print("to stdout only") +// ruleid:ios_log +NSLog("Salt used: %@", self.salt) +// ruleid:ios_log +os_log("network request started") diff --git a/tests/assets/rules/semgrep/swift/network.swift b/tests/assets/rules/semgrep/swift/network.swift new file mode 100644 index 0000000..7bfbf95 --- /dev/null +++ b/tests/assets/rules/semgrep/swift/network.swift @@ -0,0 +1,11 @@ + +// ruleid:ios_tls3_not_used +session.TLSMinimumSupportedProtocolVersion = .TLSv10 +// ruleid:ios_tls3_not_used +session.TLSMinimumSupportedProtocolVersion = tls_protocol_version_t.TLSv11 +// ruleid:ios_tls12_used +session.TLSMinimumSupportedProtocolVersion = tls_protocol_version_t.TLSv12 +// ruleid:ios_dtls1_used +session.TLSMinimumSupportedProtocolVersion = .DTLSv10 +// ruleid:ios_depr_tls_min +session.tlsMinimumSupportedProtocol = .tlsProtocol12 diff --git a/tests/assets/rules/semgrep/swift/secrets.swift b/tests/assets/rules/semgrep/swift/secrets.swift new file mode 100644 index 0000000..168e37d --- /dev/null +++ b/tests/assets/rules/semgrep/swift/secrets.swift @@ -0,0 +1,14 @@ + +// ok:ios_hardcoded_secret +private static let languageKey = "languageKey" +// ok:ios_hardcoded_secret +private let leadsLoggedKey = "leadsLogged_Key" + +// ruleid:ios_hardcoded_secret +let password = "s3cret" +// ruleid:ios_hardcoded_secret +let key = "sk-live-abc123" +// ruleid:ios_hardcoded_secret +let api_key = "sk-live-abc123" +// ruleid:ios_hardcoded_secret +let secretKey = "abc" diff --git a/tests/assets/rules/semgrep/swift/storage.swift b/tests/assets/rules/semgrep/swift/storage.swift new file mode 100644 index 0000000..bf7febe --- /dev/null +++ b/tests/assets/rules/semgrep/swift/storage.swift @@ -0,0 +1,5 @@ + +// ruleid:ios_file_no_special +let opts: Data.WritingOptions = .noFileProtection +// ruleid:ios_general_paste +_ = UIPasteboard.generalPasteboard diff --git a/tests/assets/rules/semgrep/swift/webview.swift b/tests/assets/rules/semgrep/swift/webview.swift new file mode 100644 index 0000000..bbf3086 --- /dev/null +++ b/tests/assets/rules/semgrep/swift/webview.swift @@ -0,0 +1,7 @@ + +func load(webView: WKWebView, html: String) { + // ruleid:ios_load_html_string + webView.loadHTMLString(html, baseURL: nil) +} +// ruleid:ios_uiwebview +let legacy = UIWebView() diff --git a/tests/assets/src/android_layout/safe_login.xml b/tests/assets/src/android_layout/safe_login.xml new file mode 100644 index 0000000..500b03b --- /dev/null +++ b/tests/assets/src/android_layout/safe_login.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/tests/assets/src/android_layout/unsafe_login.xml b/tests/assets/src/android_layout/unsafe_login.xml new file mode 100644 index 0000000..9e3e527 --- /dev/null +++ b/tests/assets/src/android_layout/unsafe_login.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/tests/assets/src/android_new_rules/JavaPorts.kt b/tests/assets/src/android_new_rules/JavaPorts.kt new file mode 100644 index 0000000..a124644 --- /dev/null +++ b/tests/assets/src/android_new_rules/JavaPorts.kt @@ -0,0 +1,66 @@ + +import android.hardware.biometrics.BiometricPrompt +import android.webkit.WebSettings +import java.beans.XMLDecoder +import java.io.InputStream +import java.io.ObjectInputStream +import java.security.KeyPairGenerator +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import javax.net.ssl.SSLContext +import javax.xml.stream.XMLInputFactory + +class UnsafeBio : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + unlockAccount() + } +} + +fun hardcodedAes(cipher: Cipher) { + val secret = SecretKeySpec("hardcoded".toByteArray(), "AES") + cipher.init(Cipher.ENCRYPT_MODE, secret) +} + +fun staticCbcIv(key: String) { + val bytesIV = "static-iv-value".toByteArray() + val iv = IvParameterSpec(bytesIV) + Cipher.getInstance("AES/CBC/PKCS5PADDING") +} + +fun legacySsl() { + SSLContext.getInstance("SSLv3") +} + +fun weakRsa() { + val kp = KeyPairGenerator.getInstance("RSA") + kp.initialize(512) +} + +fun allowFiles(settings: WebSettings) { + settings.allowFileAccess = true +} + +fun runCommand(user: String) { + Runtime.getRuntime().exec("id " + user) +} + +fun deserialize(stream: InputStream): Any { + return ObjectInputStream(stream).readObject() +} + +fun openXml(): XMLInputFactory { + val factory = XMLInputFactory.newFactory() + return factory +} + +fun enableXxe(factory: XMLInputFactory) { + factory.setProperty("javax.xml.stream.isSupportingExternalEntities", true) +} + +fun decodeXml(stream: InputStream): Any { + return XMLDecoder(stream).readObject() +} + +private fun unlockAccount() {} diff --git a/tests/assets/src/android_new_rules/NewRules.kt b/tests/assets/src/android_new_rules/NewRules.kt new file mode 100644 index 0000000..d8ac5e9 --- /dev/null +++ b/tests/assets/src/android_new_rules/NewRules.kt @@ -0,0 +1,25 @@ +import android.text.InputType +import javax.net.ssl.SSLContext + +fun insecureTls() { + SSLContext.getInstance("TLSv1.1") +} + +fun weakSuite(socket: javax.net.ssl.SSLSocket) { + socket.setEnabledCipherSuites( + arrayOf("TLS_RSA_WITH_3DES_EDE_CBC_SHA"), + ) +} + +fun configureInput(passwordField: android.widget.EditText) { + passwordField.inputType = InputType.TYPE_CLASS_TEXT +} + +fun encryptByte(value: Int, key: Int) = value xor key + +fun notifySecret( + builder: androidx.core.app.NotificationCompat.Builder, + oneTimePassword: String, +) { + builder.setContentText(oneTimePassword) +} diff --git a/tests/assets/src/dot_mobsf/.mobsf b/tests/assets/src/dot_mobsf/.mobsf index 7ac4e36..3d0a51e 100644 --- a/tests/assets/src/dot_mobsf/.mobsf +++ b/tests/assets/src/dot_mobsf/.mobsf @@ -16,4 +16,8 @@ - android_certificate_transparency - android_safetynet - android_ssl_pinning - - android_tapjacking \ No newline at end of file + - android_tapjacking + + severity-overrides: + default_http_client_tls: ERROR + android_kotlin_hiddenui: INFO diff --git a/tests/assets/src/dot_mobsf/scan_but_ignore2.kt b/tests/assets/src/dot_mobsf/scan_but_ignore2.kt new file mode 100644 index 0000000..68e8ae8 --- /dev/null +++ b/tests/assets/src/dot_mobsf/scan_but_ignore2.kt @@ -0,0 +1,12 @@ + private fun showN(show: Boolean) { + + xyz.visibility = if (show) View.GONE else View.VISIBLE + xyz.animate() + +} + +Log.e("foo", foo.toString()) + + + +android.secret_key="supersecret key" // mobsf-ignore: android_kotlin_hardcoded diff --git a/tests/assets/src/ios_plist/Info.plist b/tests/assets/src/ios_plist/Info.plist new file mode 100644 index 0000000..4f0565e --- /dev/null +++ b/tests/assets/src/ios_plist/Info.plist @@ -0,0 +1,44 @@ + + + + + CFBundleIdentifier + org.mobsfscan.ats-test + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsArbitraryLoadsForMedia + + NSAllowsArbitraryLoadsInWebContent + + NSAllowsLocalNetworking + + NSExceptionDomains + + insecure.example + + NSExceptionAllowsInsecureHTTPLoads + + NSExceptionMinimumTLSVersion + TLSv1.1 + NSExceptionRequiresForwardSecrecy + + NSRequiresCertificateTransparency + + + tls12.example + + NSExceptionMinimumTLSVersion + TLSv1.2 + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + + + diff --git a/tests/assets/src/java_best_practices_present/ControlsPresent.java b/tests/assets/src/java_best_practices_present/ControlsPresent.java new file mode 100644 index 0000000..11374b8 --- /dev/null +++ b/tests/assets/src/java_best_practices_present/ControlsPresent.java @@ -0,0 +1,16 @@ +import com.google.android.gms.safetynet.SafetyNetApi; + +import android.view.View; +import android.view.WindowManager; + +import okhttp3.CertificatePinner; + +class ControlsPresent extends Activity { + void enableControls(View view, Device device) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); + device.isDeviceRooted(); + view.setFilterTouchesWhenObscured(true); + new CTInterceptorBuilder(); + new CertificatePinner.Builder(); + } +} diff --git a/tests/assets/src/kotlin_best_practices_present/ControlsPresent.kt b/tests/assets/src/kotlin_best_practices_present/ControlsPresent.kt new file mode 100644 index 0000000..092af4e --- /dev/null +++ b/tests/assets/src/kotlin_best_practices_present/ControlsPresent.kt @@ -0,0 +1,13 @@ + +import android.view.WindowManager +import com.google.android.gms.safetynet.SafetyNet +import okhttp3.CertificatePinner + +fun enableControls(window: android.view.Window, view: android.view.View, device: Device) { + SafetyNet.getClient(context).attest(nonce, apiKey) + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + device.isDeviceRooted() + view.setFilterTouchesWhenObscured(true) + CTInterceptorBuilder() + CertificatePinner.Builder().add("example.com", "sha256/AAA=").build() +} diff --git a/tests/assets/src/kotlin_long_secret/LongKey.kt b/tests/assets/src/kotlin_long_secret/LongKey.kt new file mode 100644 index 0000000..6767329 --- /dev/null +++ b/tests/assets/src/kotlin_long_secret/LongKey.kt @@ -0,0 +1,2 @@ +// Issue #88: long hex KEY must still be detected (was capped at 100 chars). +private val KEY = "b4071281f217866037bb02f1d125ab5c2533eca404e95b9f19f097227bcaad4fffb8aeda6042f4225623a1a23e50ed669ca508b570ba0f53306d6eb7b04941c9f3047e564ec9c0afef3ecadf5493edb6c0b4432ecde5f1f644e7dafa727774d3a43f472845d855113f0bb7ffe79bf80efa6bdb0f53607f691bc1ad65d3503c7b8847e6a50f884abf017047ad4144c6f1561c4b5653b7b8251db0f9757ab55562dd432dbd75479d5282cf08658f4fe52b175d7fc476b59e625abb057d9413d62d080a5050a47795536f594407aaa89f1df88abba879d091e81abe37ef74bb01912df82e4b10ea3e25bba80f837d0153917442c611f411093fbacd30aab4158719" diff --git a/tests/assets/src/swift/swift.swift b/tests/assets/src/swift/swift.swift index 2586392..6e4fa16 100644 --- a/tests/assets/src/swift/swift.swift +++ b/tests/assets/src/swift/swift.swift @@ -1 +1,14 @@ - print("Salt used: \(self.salt)\n") \ No newline at end of file +print("Salt used: \(self.salt)\n") +NSLog("Salt used: %@", self.salt) +os_log("network request started") + +// Should not match ios_hardcoded_secret (#111) +private static let APP_VERSION_KEY = "AppVersionStringKey" +private static let languageKey = "languageKey" +private let leadsLoggedKey = "leadsLogged_Key" + +// Should match ios_hardcoded_secret +let password = "s3cret" +let key = "sk-live-abc123" +let api_key = "sk-live-abc123" +let secretKey = "abc" diff --git a/tests/assets/src/swift_best_practices_present/ControlsPresent.swift b/tests/assets/src/swift_best_practices_present/ControlsPresent.swift new file mode 100644 index 0000000..656e3c6 --- /dev/null +++ b/tests/assets/src/swift_best_practices_present/ControlsPresent.swift @@ -0,0 +1,8 @@ + +func enableControls(field: UITextField) { + let jb = "/Applications/Cydia.app" + if extensionPointIdentifier == UIExtensionPointIdentifier.keyboard {} + field.autocorrectionType = .no + let markers = ["FridaGadget", "cynject", "libcycript", "/usr/sbin/frida-server"] + TrustKit.initSharedInstance(with: [:]) +} diff --git a/tests/assets/src/swift_ignore/IgnoreLog.swift b/tests/assets/src/swift_ignore/IgnoreLog.swift new file mode 100644 index 0000000..100ea11 --- /dev/null +++ b/tests/assets/src/swift_ignore/IgnoreLog.swift @@ -0,0 +1,6 @@ +import Foundation + +NSLog("bol suppressed") // mobsf-ignore: ios_log + NSLog("indented suppressed") // mobsf-ignore: ios_log + NSLog("still reported") + os_log("also reported") diff --git a/tests/assets/src/xml/nsc_multiple_domain_config_siblings.xml b/tests/assets/src/xml/nsc_multiple_domain_config_siblings.xml new file mode 100644 index 0000000..dbb5e37 --- /dev/null +++ b/tests/assets/src/xml/nsc_multiple_domain_config_siblings.xml @@ -0,0 +1,13 @@ + + + + + + + domainA + + + + domainB + + diff --git a/tests/unit/setup_test.py b/tests/unit/setup_test.py index 8c6b998..61448b7 100644 --- a/tests/unit/setup_test.py +++ b/tests/unit/setup_test.py @@ -10,13 +10,17 @@ def scanner(paths): def get_paths(): base_dir = Path(__file__).parents[1] / 'assets' / 'src' + android_layout = base_dir / 'android_layout' dot_file = base_dir / 'dot_mobsf' + android_new_rules = base_dir / 'android_new_rules' java = base_dir / 'java' kotlin = base_dir / 'kotlin' swift = base_dir / 'swift' objc = base_dir / 'objc' xmlp = base_dir / 'xml' paths = { + 'android_layout': android_layout, + 'android_new_rules': android_new_rules, 'dot_file': dot_file, 'java': java, 'kotlin': kotlin, diff --git a/tests/unit/test_dotfile.py b/tests/unit/test_dotfile.py index 6b4e275..346fe16 100644 --- a/tests/unit/test_dotfile.py +++ b/tests/unit/test_dotfile.py @@ -10,6 +10,12 @@ 'android_kotlin_hiddenui', ] +# From tests/assets/src/dot_mobsf/.mobsf severity-overrides +SEVERITY_OVERRIDES = { + 'default_http_client_tls': 'ERROR', + 'android_kotlin_hiddenui': 'INFO', +} + def test_mobsfscan_dotfile(): paths = get_paths() @@ -19,3 +25,5 @@ def test_mobsfscan_dotfile(): triggered.sort() SCAN_ONLY.sort() assert triggered == SCAN_ONLY + for rule_id, severity in SEVERITY_OVERRIDES.items(): + assert res['results'][rule_id]['metadata']['severity'] == severity diff --git a/tests/unit/test_gitlab_sast.py b/tests/unit/test_gitlab_sast.py new file mode 100644 index 0000000..0f47c45 --- /dev/null +++ b/tests/unit/test_gitlab_sast.py @@ -0,0 +1,105 @@ +# -*- coding: utf_8 -*- +"""Tests for GitLab SAST report formatter.""" +import json + +from mobsfscan import __version__ +from mobsfscan.formatters.gitlab_sast import ( + SCHEMA_VERSION, + gitlab_sast_output, + gitlab_severity, +) + + +def test_gitlab_severity_mapping(): + assert gitlab_severity('ERROR') == 'Critical' + assert gitlab_severity('WARNING') == 'Medium' + assert gitlab_severity('INFO') == 'Info' + + +def test_gitlab_sast_report_shape(tmp_path): + scan_results = { + 'results': { + 'ios_cert_pinning': { + 'metadata': { + 'description': ( + 'This app does not have Certificate Pinning ' + 'implemented in code.'), + 'severity': 'INFO', + 'cwe': 'CWE-295: Improper Certificate Validation', + 'owasp-mobile': 'M3: Insecure Communication', + 'masvs': 'MASVS-NETWORK-1', + 'reference': 'https://example.com/pinning', + }, + 'files': [{ + 'file_path': 'app/Network.swift', + 'match_lines': [12, 14], + 'match_position': [1, 20], + 'match_string': 'URLSession', + }], + }, + 'webview_mixed_content': { + 'metadata': { + 'description': ( + 'WebView is configured with ' + 'MIXED_CONTENT_ALWAYS_ALLOW.'), + 'severity': 'ERROR', + 'cwe': 'CWE-319: Cleartext Transmission of Sensitive Information', + 'owasp-mobile': 'M3: Insecure Communication', + 'masvs': 'MSTG-NETWORK-1', + 'reference': 'https://example.com/mixed', + }, + 'files': [{ + 'file_path': 'app/Web.java', + 'match_lines': [10, 10], + 'match_position': [5, 40], + 'match_string': 'setMixedContentMode', + }], + }, + }, + } + outfile = tmp_path / 'gl-sast-report.json' + gitlab_sast_output(str(outfile), scan_results, __version__) + report = json.loads(outfile.read_text()) + + assert report['version'] == SCHEMA_VERSION + assert report['scan']['type'] == 'sast' + assert report['scan']['scanner']['id'] == 'mobsfscan' + assert report['scan']['scanner']['version'] == __version__ + assert len(report['vulnerabilities']) == 2 + + by_file = {v['location']['file']: v for v in report['vulnerabilities']} + pinning = by_file['app/Network.swift'] + assert pinning['severity'] == 'Info' + assert 'Certificate Pinning' in pinning['name'] + assert pinning['location']['start_line'] == 12 + assert pinning['location']['end_line'] == 14 + types = {i['type'] for i in pinning['identifiers']} + assert 'mobsfscan_rule_id' in types + assert 'cwe' in types + assert 'owasp_mobile' in types + assert 'masvs' in types + assert pinning['links'][0]['url'] == 'https://example.com/pinning' + + mixed = by_file['app/Web.java'] + assert mixed['severity'] == 'Critical' + assert mixed['identifiers'][0]['value'] == 'webview_mixed_content' + + +def test_gitlab_sast_missing_control_location(tmp_path): + scan_results = { + 'results': { + 'ios_cert_pinning': { + 'metadata': { + 'description': 'Missing certificate pinning.', + 'severity': 'INFO', + 'cwe': 'cwe-295', + }, + }, + }, + } + outfile = tmp_path / 'gl-sast-report.json' + gitlab_sast_output(str(outfile), scan_results, __version__) + report = json.loads(outfile.read_text()) + vuln = report['vulnerabilities'][0] + assert vuln['location']['file'] == '.' + assert vuln['location']['start_line'] == 1 diff --git a/tests/unit/test_hardcoded_secret.py b/tests/unit/test_hardcoded_secret.py new file mode 100644 index 0000000..4b6f008 --- /dev/null +++ b/tests/unit/test_hardcoded_secret.py @@ -0,0 +1,46 @@ +# -*- coding: utf_8 -*- +"""Tests for hardcoded secret rules (#88, #111).""" +from pathlib import Path + +from mobsfscan.mobsfscan import MobSFScan + + +def test_ios_hardcoded_secret_skips_lookup_key_names(): + src = Path(__file__).resolve().parents[1] / 'assets' / 'src' / 'swift' + res = MobSFScan([str(src)], True, mp='thread').scan() + finding = res['results'].get('ios_hardcoded_secret') + assert finding is not None + files = finding.get('files') or [] + lines = {f.get('match_lines', (None, None))[0] for f in files} + src_text = (src / 'swift.swift').read_text(encoding='utf-8').splitlines() + + # Lookup-style *Key names must not be flagged (#111). + for lineno in (6, 7, 8): + assert lineno not in lines + assert 'Key' in src_text[lineno - 1] + + # Real secrets should be flagged (lines from fixture). + assert {11, 12, 13, 14}.issubset(lines) + # Prefer raw source when Semgrep CE redacts match lines. + assert 'password' in src_text[10].lower() + assert 'api_key' in src_text[12].lower() + assert 'secretkey' in src_text[13].lower().replace('_', '') + + +def test_kotlin_long_hardcoded_key_detected(): + """Issue #88: values longer than 100 chars were false negatives.""" + src = ( + Path(__file__).resolve().parents[1] + / 'assets' / 'src' / 'kotlin_long_secret') + res = MobSFScan([str(src)], True, mp='thread').scan() + finding = res['results'].get('android_kotlin_hardcoded') + assert finding is not None + matches = finding.get('files') or [] + assert matches + assert any( + (m.get('file_path') or '').endswith('LongKey.kt') + for m in matches) + # Prefer raw source when Semgrep CE redacts match lines. + src_text = (src / 'LongKey.kt').read_text(encoding='utf-8') + assert 'KEY' in src_text + assert len(src_text) > 100 diff --git a/tests/unit/test_ignore_comments.py b/tests/unit/test_ignore_comments.py new file mode 100644 index 0000000..3a1c6c9 --- /dev/null +++ b/tests/unit/test_ignore_comments.py @@ -0,0 +1,44 @@ +# -*- coding: utf_8 -*- +"""Tests for mobsf-ignore comment suppressions (#104, #107).""" +from pathlib import Path + +from mobsfscan.mobsfscan import MobSFScan + +from .setup_test import get_paths + + +def test_line_ignores_rule_parsing(): + assert MobSFScan._line_ignores_rule( + 'x // mobsf-ignore: ios_log', 'ios_log') + assert MobSFScan._line_ignores_rule( + 'x // mobsf-ignore: ios_log, ios_hardcoded_secret', + 'ios_hardcoded_secret') + assert not MobSFScan._line_ignores_rule( + 'x // mobsf-ignore: ios_log', 'ios_logging') + assert not MobSFScan._line_ignores_rule( + 'x // mobsf-ignore: ios_logger', 'ios_log') + assert not MobSFScan._line_ignores_rule('NSLog("x")', 'ios_log') + + +def test_multiple_files_same_rule_suppressed(): + """Issue #104: suppressions across files must all apply.""" + paths = get_paths() + res = MobSFScan([str(paths['dot_file'])], True, mp='thread').scan() + assert 'android_kotlin_hardcoded' not in res['results'] + + +def test_ios_log_line_level_and_bol_ignore(): + """Issue #107: BOL ignore works; other lines in the file still report.""" + src = Path(__file__).resolve().parents[1] / 'assets' / 'src' / 'swift_ignore' + scan = MobSFScan([str(src)], True, mp='thread') + res = scan.scan() + files = res['results']['ios_log']['files'] + # Two suppressed NSLog lines removed; unsuppressed NSLog + os_log remain + assert len(files) == 2 + lines = sorted(f['match_lines'][0] for f in files) + assert lines == [5, 6] + src_text = (src / 'IgnoreLog.swift').read_text(encoding='utf-8') + assert 'NSLog("still reported")' in src_text + assert 'os_log("also reported")' in src_text + for match in files: + assert not scan.suppress_pm_comments(match, 'ios_log') diff --git a/tests/unit/test_ios_plist.py b/tests/unit/test_ios_plist.py new file mode 100644 index 0000000..71be6d9 --- /dev/null +++ b/tests/unit/test_ios_plist.py @@ -0,0 +1,58 @@ +# -*- coding: utf_8 -*- +"""Tests for iOS App Transport Security Info.plist analysis.""" +from pathlib import Path +from plistlib import loads + +from mobsfscan.ios_plist import check_transport_security +from mobsfscan.mobsfscan import MobSFScan + + +EXPECTED_ATS_RULES = { + 'ios_ats_arbitrary_loads', + 'ios_ats_arbitrary_loads_for_media', + 'ios_ats_arbitrary_loads_in_web_content', + 'ios_ats_local_networking', + 'ios_ats_insecure_http_loads', + 'ios_ats_weak_tls', + 'ios_ats_tls12', + 'ios_ats_forward_secrecy_disabled', + 'ios_ats_certificate_transparency_disabled', +} + + +def test_ats_info_plist_scan(): + src = Path(__file__).resolve().parents[1] / 'assets' / 'src' / 'ios_plist' + res = MobSFScan([str(src)], True, scan_type='ios', mp='thread').scan() + assert set(res['results']) == EXPECTED_ATS_RULES + insecure = res['results']['ios_ats_insecure_http_loads'] + assert 'insecure.example' in insecure['metadata']['description'] + assert 'localhost' not in insecure['metadata']['description'] + # Same path normalization as XML (#109): prefer cwd-relative paths. + files = insecure.get('files') or [] + assert files + assert files[0]['file_path'].endswith('Info.plist') + assert not Path(files[0]['file_path']).is_absolute() + + +def test_ats_safe_plist_has_no_findings(): + plist = loads(b""" + + NSAppTransportSecurity + NSAllowsArbitraryLoads + NSExceptionDomains + secure.example + NSExceptionMinimumTLSVersionTLSv1.3 + NSExceptionRequiresForwardSecrecy + NSRequiresCertificateTransparency + + + + """) + assert check_transport_security('/tmp/Info.plist', plist) == [] + + +def test_non_info_plist_is_not_scanned(tmp_path): + plist = tmp_path / 'Settings.plist' + plist.write_bytes(b'') + scan = MobSFScan([str(tmp_path)], True, scan_type='ios', mp='thread') + assert scan.plists == [] diff --git a/tests/unit/test_java_best_practices.py b/tests/unit/test_java_best_practices.py new file mode 100644 index 0000000..6b0ee37 --- /dev/null +++ b/tests/unit/test_java_best_practices.py @@ -0,0 +1,84 @@ +# -*- coding: utf_8 -*- +"""Java Semgrep best-practice inversion (missing controls).""" +from pathlib import Path + +from mobsfscan.mobsfscan import MobSFScan +from mobsfscan.utils import get_best_practices + +from .setup_test import get_paths + + +JAVA_BP_IDS = { + 'android_safetynet_api', + 'android_prevent_screenshot', + 'android_root_detection', + 'android_detect_tapjacking', + 'android_certificate_transparency', + 'android_certificate_pinning', +} + + +def test_get_best_practices_java_uses_semgrep_dir(): + ids, rules = get_best_practices('.java') + assert ids == JAVA_BP_IDS + assert set(rules) == JAVA_BP_IDS + # Must not pick up kotlin/ subdirectory IDs that differ. + assert 'android_safetynet' not in ids + assert 'android_ssl_pinning' not in ids + assert 'android_tapjacking' not in ids + + +def test_java_missing_controls_reported_when_absent(): + paths = get_paths() + res = MobSFScan([str(paths['java'])], True, mp='thread').scan() + # java_vuln.java already implements certificate transparency. + present_in_fixture = {'android_certificate_transparency'} + for rule_id in JAVA_BP_IDS - present_in_fixture: + assert rule_id in res['results'] + assert not res['results'][rule_id].get('files') + for rule_id in present_in_fixture: + assert rule_id not in res['results'] + + +def test_java_present_controls_are_inverted_away(): + src = ( + Path(__file__).resolve().parents[1] + / 'assets' / 'src' / 'java_best_practices_present') + res = MobSFScan([str(src)], True, mp='thread').scan() + for rule_id in JAVA_BP_IDS: + assert rule_id not in res['results'] + + +def test_type_android_on_java_does_not_leak_or_false_missing(): + """--type android must invert Java BP IDs, not only Kotlin's.""" + src = ( + Path(__file__).resolve().parents[1] + / 'assets' / 'src' / 'java_best_practices_present') + res = MobSFScan( + [str(src)], True, scan_type='android', mp='thread').scan() + for rule_id in JAVA_BP_IDS: + assert rule_id not in res['results'] + # Kotlin-only IDs must not be reported missing on a Java-only tree. + for rule_id in ( + 'android_safetynet', + 'android_ssl_pinning', + 'android_tapjacking'): + assert rule_id not in res['results'] + + +def test_mixed_java_kotlin_present_does_not_leak_kotlin_bp(): + """Presence hits for either Android dialect must be inverted away.""" + base = Path(__file__).resolve().parents[1] / 'assets' / 'src' + res = MobSFScan( + [ + str(base / 'java_best_practices_present'), + str(base / 'kotlin_best_practices_present'), + ], + True, + mp='thread', + ).scan() + for rule_id in JAVA_BP_IDS | { + 'android_safetynet', + 'android_ssl_pinning', + 'android_tapjacking'}: + assert rule_id not in res['results'] diff --git a/tests/unit/test_kotlin_best_practices.py b/tests/unit/test_kotlin_best_practices.py new file mode 100644 index 0000000..655f51a --- /dev/null +++ b/tests/unit/test_kotlin_best_practices.py @@ -0,0 +1,54 @@ +# -*- coding: utf_8 -*- +"""Kotlin Semgrep best-practice inversion (missing controls).""" +from pathlib import Path + +from mobsfscan.mobsfscan import MobSFScan +from mobsfscan.utils import get_best_practices + +from .setup_test import get_paths + + +KOTLIN_BP_IDS = { + 'android_safetynet', + 'android_prevent_screenshot', + 'android_root_detection', + 'android_tapjacking', + 'android_certificate_transparency', + 'android_ssl_pinning', +} + + +def test_get_best_practices_kotlin_uses_semgrep_dir(): + ids, rules = get_best_practices('.kt') + assert ids == KOTLIN_BP_IDS + assert set(rules) == KOTLIN_BP_IDS + # Java loader must not pick up kotlin/ subdirectory IDs that differ. + java_ids, _ = get_best_practices('.java') + assert 'android_safetynet' not in java_ids + assert 'android_safetynet_api' in java_ids + assert 'android_ssl_pinning' not in java_ids + assert 'android_certificate_pinning' in java_ids + + +def test_kotlin_missing_controls_reported_when_absent(): + paths = get_paths() + res = MobSFScan([str(paths['kotlin'])], True, mp='thread').scan() + for rule_id in KOTLIN_BP_IDS: + assert rule_id in res['results'] + # Missing controls have metadata only (no file matches). + assert not res['results'][rule_id].get('files') + + +def test_kotlin_present_controls_are_inverted_away(): + src = ( + Path(__file__).resolve().parents[1] + / 'assets' / 'src' / 'kotlin_best_practices_present') + res = MobSFScan([str(src)], True, mp='thread').scan() + for rule_id in KOTLIN_BP_IDS: + assert rule_id not in res['results'] + # Java-only BP IDs must not be reported missing on a Kotlin-only tree. + for rule_id in ( + 'android_safetynet_api', + 'android_certificate_pinning', + 'android_detect_tapjacking'): + assert rule_id not in res['results'] diff --git a/tests/unit/test_matcher.py b/tests/unit/test_matcher.py index 9f8ab3f..fca9b89 100644 --- a/tests/unit/test_matcher.py +++ b/tests/unit/test_matcher.py @@ -11,6 +11,30 @@ def test_kotlin(): assert len(res['results'].keys()) != 0 +def test_new_android_kotlin_rules(): + paths = get_paths() + res = scanner([paths['android_new_rules']]) + expected = { + 'android_kotlin_insecure_tls_version', + 'android_kotlin_weak_tls_cipher_suite', + 'android_kotlin_sensitive_input_keyboard_cache', + 'android_kotlin_custom_xor_crypto', + 'android_kotlin_sensitive_notification', + 'android_kotlin_biometric_without_crypto', + 'android_kotlin_aes_hardcoded_key', + 'android_kotlin_cbc_static_iv', + 'android_kotlin_insecure_sslv3', + 'android_kotlin_weak_key_size', + 'android_kotlin_webview_set_allow_file_access', + 'android_kotlin_command_injection', + 'android_kotlin_object_deserialization', + 'android_kotlin_xmlinputfactory_xxe', + 'android_kotlin_xmlinputfactory_xxe_enabled', + 'android_kotlin_xml_decoder_xxe', + } + assert expected.issubset(res['results']) + + def test_ios(): paths = get_paths() diff --git a/tests/unit/test_mobsfscan.py b/tests/unit/test_mobsfscan.py index cb0d9c5..675f345 100644 --- a/tests/unit/test_mobsfscan.py +++ b/tests/unit/test_mobsfscan.py @@ -4,6 +4,7 @@ scanner, ) +from mobsfscan import __version__ from mobsfscan.formatters import ( json_fmt, sarif, @@ -12,11 +13,17 @@ EXPECTED = [ + # Java missing controls (java_vuln has CT present → not listed) 'android_safetynet_api', 'android_prevent_screenshot', 'android_certificate_pinning', 'android_root_detection', 'android_detect_tapjacking', + # Kotlin missing controls (mixed scan reports both dialects) + 'android_safetynet', + 'android_ssl_pinning', + 'android_tapjacking', + # Code findings 'android_kotlin_logging', 'android_kotlin_hiddenui', 'android_logging', @@ -36,15 +43,15 @@ def test_patterns_and_semgrep(): def json_output(res): - json_out = json_fmt.json_output(None, res, '0.0.0') + json_out = json_fmt.json_output(None, res, __version__) assert json_out is not None def sonar_output(res): - sonar_out = sonarqube.sonarqube_output(None, res, '0.0.0') + sonar_out = sonarqube.sonarqube_output(None, res, __version__) assert sonar_out is not None def sarif_output(res): - sarif_out = sarif.sarif_output(None, res, '0.0.0', '/tmp/') + sarif_out = sarif.sarif_output(None, res, __version__, '/tmp/') assert sarif_out is not None diff --git a/tests/unit/test_report_path.py b/tests/unit/test_report_path.py new file mode 100644 index 0000000..f8f934b --- /dev/null +++ b/tests/unit/test_report_path.py @@ -0,0 +1,21 @@ +# -*- coding: utf_8 -*- +"""Tests for cwd-relative finding path normalization (#109).""" +from pathlib import Path + +from mobsfscan.utils import report_path + + +def test_report_path_relativizes_under_cwd(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + nested = tmp_path / 'app' / 'src' / 'AndroidManifest.xml' + nested.parent.mkdir(parents=True) + nested.write_text('') + assert report_path(nested) == 'app/src/AndroidManifest.xml' + assert report_path(Path('app/src/AndroidManifest.xml')) == ( + 'app/src/AndroidManifest.xml') + + +def test_report_path_keeps_outside_cwd(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + outside = Path('/tmp/Info.plist') + assert report_path(outside) == '/tmp/Info.plist' diff --git a/tests/unit/test_sarif.py b/tests/unit/test_sarif.py new file mode 100644 index 0000000..cd79a48 --- /dev/null +++ b/tests/unit/test_sarif.py @@ -0,0 +1,89 @@ +# -*- coding: utf_8 -*- +"""Tests for SARIF rule naming and dashboard metadata.""" +import json + +from mobsfscan import __version__ +from mobsfscan.formatters.sarif import ( + build_tags, + format_rule_name, + sarif_output, + security_severity_score, +) + + +def test_format_rule_name_uses_description_and_cwe(): + name = format_rule_name('ios_cert_pinning', { + 'description': ( + 'This app does not have Certificate Pinning ' + 'implemented in code.'), + 'cwe': 'CWE-295: Improper Certificate Validation', + }) + assert name == ( + 'This app does not have Certificate Pinning ' + 'implemented in code (CWE-295)') + + +def test_format_rule_name_falls_back_to_id(): + assert format_rule_name('ios_cert_pinning', {}) == 'IosCertPinning' + + +def test_security_severity_prefers_cvss_then_severity(): + assert security_severity_score({'cvss': 7.4}) == '7.4' + assert security_severity_score({'severity': 'ERROR'}) == '9.0' + assert security_severity_score({'severity': 'WARNING'}) == '5.5' + assert security_severity_score({'severity': 'INFO'}) == '2.0' + + +def test_build_tags_from_metadata(): + tags = build_tags({ + 'cwe': 'CWE-295: Improper Certificate Validation', + 'owasp-mobile': 'M3: Insecure Communication', + 'masvs': 'MSTG-NETWORK-1', + }) + assert tags[0] == 'security' + assert 'external/cwe/cwe-295' in tags + assert 'owasp-mobile/m3' in tags + assert 'masvs/network-1' in tags + assert len(tags) <= 10 + + +def test_sarif_includes_dashboard_fields(tmp_path): + scan_results = { + 'results': { + 'ios_cert_pinning': { + 'metadata': { + 'description': ( + 'This app does not have Certificate Pinning ' + 'implemented in code.'), + 'severity': 'INFO', + 'cwe': 'CWE-295: Improper Certificate Validation', + 'owasp-mobile': 'M3: Insecure Communication', + 'masvs': 'MASVS-NETWORK-1', + 'reference': 'https://example.com', + }, + }, + }, + } + outfile = tmp_path / 'out.sarif' + sarif_output(str(outfile), scan_results, __version__, ['app']) + out = json.loads(outfile.read_text()) + driver = out['runs'][0]['tool']['driver'] + rule = driver['rules'][0] + result = out['runs'][0]['results'][0] + assert driver['version'] == __version__ + assert driver['semanticVersion'] == __version__ + + assert rule['id'] == 'ios_cert_pinning' + assert rule['name'] == ( + 'This app does not have Certificate Pinning ' + 'implemented in code (CWE-295)') + assert rule['shortDescription']['text'] == rule['name'] + assert 'Certificate Pinning' in rule['fullDescription']['text'] + assert 'Reference: https://example.com' in rule['help']['text'] + assert rule['defaultConfiguration']['level'] == 'note' + assert rule['properties']['security-severity'] == '2.0' + assert rule['properties']['precision'] == 'medium' + assert 'security' in rule['properties']['tags'] + assert 'external/cwe/cwe-295' in rule['properties']['tags'] + assert result['properties']['security-severity'] == '2.0' + assert 'IosCertPinning' not in rule['name'] diff --git a/tests/unit/test_severity_overrides.py b/tests/unit/test_severity_overrides.py new file mode 100644 index 0000000..9b7b525 --- /dev/null +++ b/tests/unit/test_severity_overrides.py @@ -0,0 +1,90 @@ +# -*- coding: utf_8 -*- +"""Tests for .mobsf severity-overrides (#108).""" +from mobsfscan.mobsfscan import MobSFScan +from mobsfscan.utils import ( + get_config, + normalize_severity_overrides, +) + + +def test_normalize_severity_overrides(): + assert normalize_severity_overrides(None) == {} + assert normalize_severity_overrides(['ios_log']) == {} + assert normalize_severity_overrides({ + 'ios_log': 'error', + 'android_logging': ' WARNING ', + 'bad': 'critical', + '': 'ERROR', + }) == { + 'ios_log': 'ERROR', + 'android_logging': 'WARNING', + } + + +def test_get_config_reads_severity_overrides(tmp_path): + cfg = tmp_path / '.mobsf' + cfg.write_text( + '---\n' + '- severity-overrides:\n' + ' ios_log: ERROR\n' + ' android_logging: warning\n', + encoding='utf-8') + options = get_config([str(tmp_path)], False) + assert options['severity_overrides'] == { + 'ios_log': 'ERROR', + 'android_logging': 'WARNING', + } + + +def test_post_override_severities_before_filter(tmp_path): + cfg = tmp_path / 'custom.mobsf' + cfg.write_text( + '---\n' + '- severity-overrides:\n' + ' ios_log: ERROR\n' + ' severity-filter:\n' + ' - ERROR\n', + encoding='utf-8') + scan = MobSFScan([str(tmp_path)], True, config=str(cfg)) + scan.result = { + 'results': { + 'ios_log': { + 'metadata': { + 'description': 'logs', + 'severity': 'INFO', + }, + }, + 'other_rule': { + 'metadata': { + 'description': 'x', + 'severity': 'WARNING', + }, + }, + }, + 'errors': [], + } + scan.post_override_severities() + assert scan.result['results']['ios_log']['metadata']['severity'] == 'ERROR' + scan.post_ignore_rules_by_severity() + assert 'ios_log' in scan.result['results'] + assert 'other_rule' not in scan.result['results'] + + +def test_severity_override_ignored_for_missing_rule(tmp_path): + cfg = tmp_path / '.mobsf' + cfg.write_text( + '---\n' + '- severity-overrides:\n' + ' missing_rule: ERROR\n', + encoding='utf-8') + scan = MobSFScan([str(tmp_path)], True, config=str(cfg)) + scan.result = { + 'results': { + 'ios_log': { + 'metadata': {'severity': 'INFO'}, + }, + }, + 'errors': [], + } + scan.post_override_severities() + assert scan.result['results']['ios_log']['metadata']['severity'] == 'INFO' diff --git a/tests/unit/test_sonarqube.py b/tests/unit/test_sonarqube.py new file mode 100644 index 0000000..f28f35c --- /dev/null +++ b/tests/unit/test_sonarqube.py @@ -0,0 +1,105 @@ +# -*- coding: utf_8 -*- +"""Tests for SonarQube generic issue formatter (10.3+).""" +import json + +from mobsfscan import __version__ +from mobsfscan.formatters.sonarqube import ( + IMPACT_SEVERITY_MAP, + SEVERITY_MAP, + sonarqube_output, +) + + +def test_severity_maps(): + assert SEVERITY_MAP['ERROR'] == 'CRITICAL' + assert SEVERITY_MAP['WARNING'] == 'MAJOR' + assert IMPACT_SEVERITY_MAP['ERROR'] == 'HIGH' + assert IMPACT_SEVERITY_MAP['INFO'] == 'LOW' + + +def test_sonarqube_new_format_shape(): + scan_results = { + 'results': { + 'ios_cert_pinning': { + 'metadata': { + 'description': ( + 'This app does not have Certificate Pinning ' + 'implemented in code.'), + 'severity': 'INFO', + 'cwe': 'CWE-295: Improper Certificate Validation', + }, + 'files': [], + }, + 'webview_mixed_content': { + 'metadata': { + 'description': ( + 'WebView is configured with ' + 'MIXED_CONTENT_ALWAYS_ALLOW.'), + 'severity': 'ERROR', + 'cwe': 'CWE-319: Cleartext Transmission of Sensitive Information', + }, + 'files': [ + { + 'file_path': 'app/MainActivity.java', + 'match_lines': [10, 12], + 'match_position': [4, 40], + 'match_string': 'MIXED_CONTENT_ALWAYS_ALLOW', + }, + { + 'file_path': 'app/Other.java', + 'match_lines': [5, 5], + 'match_string': 'setMixedContentMode', + }, + ], + }, + }, + } + raw = sonarqube_output(None, scan_results, __version__) + report = json.loads(raw) + + assert set(report.keys()) == {'rules', 'issues'} + assert 'mobsfscan_version' not in report + assert len(report['rules']) == 2 + assert len(report['issues']) == 2 + + rules_by_id = {r['id']: r for r in report['rules']} + pinning = rules_by_id['ios_cert_pinning'] + assert pinning['engineId'] == 'mobsfscan' + assert pinning['type'] == 'VULNERABILITY' + assert pinning['severity'] == 'INFO' + assert pinning['cleanCodeAttribute'] == 'TRUSTWORTHY' + assert pinning['impacts'] == [{ + 'softwareQuality': 'SECURITY', + 'severity': 'LOW', + }] + assert 'Certificate Pinning' in pinning['name'] + assert pinning['description'].startswith('This app does not') + + mixed = rules_by_id['webview_mixed_content'] + assert mixed['severity'] == 'CRITICAL' + assert mixed['impacts'][0]['severity'] == 'HIGH' + + issues_by_rule = {i['ruleId']: i for i in report['issues']} + missing = issues_by_rule['ios_cert_pinning'] + assert missing['primaryLocation']['filePath'] == '.' + assert missing['primaryLocation']['textRange']['startLine'] == 1 + assert 'secondaryLocations' not in missing + + vuln = issues_by_rule['webview_mixed_content'] + assert vuln['effortMinutes'] == 60 + primary = vuln['primaryLocation'] + assert primary['filePath'] == 'app/MainActivity.java' + assert primary['textRange'] == { + 'startLine': 10, + 'endLine': 12, + 'startColumn': 4, + 'endColumn': 40, + } + assert 'MIXED_CONTENT_ALWAYS_ALLOW' in primary['message'] + assert len(vuln['secondaryLocations']) == 1 + assert vuln['secondaryLocations'][0]['filePath'] == 'app/Other.java' + + +def test_sonarqube_empty_results(): + report = json.loads(sonarqube_output(None, {'results': {}}, __version__)) + assert report == {'rules': [], 'issues': []} diff --git a/tests/unit/test_swift_best_practices.py b/tests/unit/test_swift_best_practices.py new file mode 100644 index 0000000..fcbdd2d --- /dev/null +++ b/tests/unit/test_swift_best_practices.py @@ -0,0 +1,45 @@ +# -*- coding: utf_8 -*- +"""Swift Semgrep best-practice inversion (missing controls).""" +from pathlib import Path + +from mobsfscan.mobsfscan import MobSFScan +from mobsfscan.utils import get_best_practices + + +SWIFT_BP_IDS = { + 'ios_jailbreak_detect', + 'ios_custom_keyboard_disabled', + 'ios_keyboard_cache', + 'ios_detect_reversing', + 'ios_cert_pinning', +} + + +def test_get_best_practices_swift_uses_semgrep_dir(): + ids, rules = get_best_practices('.swift') + assert ids == SWIFT_BP_IDS + assert set(rules) == SWIFT_BP_IDS + + +def test_swift_missing_controls_reported_when_absent(): + # Use a swift file without resilience controls. + src = Path(__file__).resolve().parents[1] / 'assets' / 'src' / 'swift' + res = MobSFScan([str(src)], True, mp='thread').scan() + for rule_id in SWIFT_BP_IDS: + assert rule_id in res['results'] + assert not res['results'][rule_id].get('files') + + +def test_swift_present_controls_are_inverted_away(): + src = ( + Path(__file__).resolve().parents[1] + / 'assets' / 'src' / 'swift_best_practices_present') + res = MobSFScan([str(src)], True, mp='thread').scan() + for rule_id in SWIFT_BP_IDS: + assert rule_id not in res['results'] + + +def test_objc_best_practices_still_regex(): + ids, _ = get_best_practices('.m') + assert 'ios_jailbreak_detect' in ids + assert 'ios_mach_ports' in ids diff --git a/tests/unit/test_xml.py b/tests/unit/test_xml.py index df00a75..d56743a 100644 --- a/tests/unit/test_xml.py +++ b/tests/unit/test_xml.py @@ -1,4 +1,8 @@ """Test XML checks rules.""" +from pathlib import Path + +from mobsfscan.mobsfscan import MobSFScan + from .setup_test import ( get_paths, scanner, @@ -9,3 +13,23 @@ def test_xml(): paths = get_paths() res = scanner([paths['xml']]) assert len(res['results'].keys()) == 5 + + +def test_multiple_sibling_domain_configs(): + """Issue #87: multiple domain-config blocks must not crash.""" + xml_dir = Path(__file__).resolve().parents[1] / 'assets' / 'src' / 'xml' + res = MobSFScan([str(xml_dir)], True, mp='thread').scan() + finding = res['results']['android_manifest_domain_config_cleartext'] + paths = [f.get('file_path') or '' for f in finding.get('files') or []] + assert any('nsc_multiple_domain_config_siblings.xml' in p for p in paths) + + +def test_sensitive_layout_input_keyboard_cache(): + paths = get_paths() + res = scanner([paths['android_layout']]) + finding = res['results']['android_layout_sensitive_input_keyboard_cache'] + files = [item['file_path'] for item in finding['files']] + assert len(files) == 1 + assert files[0].endswith('unsafe_login.xml') + # #109: XML findings should prefer cwd-relative paths (like source). + assert not Path(files[0]).is_absolute()