From 49d1fc92b2a805dba3eff9f6c938135fe3f48a26 Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 19:20:55 -0700 Subject: [PATCH 01/19] Bump to 0.4.6 and port recent MobSF WebView rules. Add mixed-content detection for Java/Kotlin and tighten SharedPreferences world mode matching from MobSF. Co-authored-by: Cursor --- mobsfscan/__init__.py | 2 +- .../patterns/android/kotlin/kotlin_rules.yaml | 18 +++++++++++++ .../webview/webview_mixed_content.yaml | 26 ++++++++++++++++++ .../webview/webview_mixed_content.java | 27 +++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 mobsfscan/rules/semgrep/webview/webview_mixed_content.yaml create mode 100644 tests/assets/rules/semgrep/webview/webview_mixed_content.java diff --git a/mobsfscan/__init__.py b/mobsfscan/__init__.py index 10e46c2..c509bfa 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__ = '0.4.6' __version_info__ = tuple(int(i) for i in __version__.split('.')) __all__ = [ '__title__', diff --git a/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml b/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml index 2e46f27..0a98e8d 100644 --- a/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml +++ b/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml @@ -125,6 +125,22 @@ 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 + 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. + type: Regex + pattern: setMixedContentMode\(.{0,48}MIXED_CONTENT_ALWAYS_ALLOW + severity: ERROR + input_case: exact + metadata: + cvss: 7.4 + 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_sql_raw_query message: >- App uses SQLite Database and execute raw SQL query. Untrusted user input in @@ -217,6 +233,7 @@ type: RegexOr pattern: - MODE_WORLD_WRITABLE + - \.getSharedPreferences\([^)]{0,50}?,\s*2\s*\) - 'openFileOutput\(\s*".{1,48}"\s*,\s*2\s*\)' severity: WARNING input_case: exact @@ -230,6 +247,7 @@ type: RegexOr pattern: - MODE_WORLD_READABLE + - \.getSharedPreferences\([^)]{0,50}?,\s*1\s*\) - 'openFileOutput\(\s*".{1,48}"\s*,\s*1\s*\)' severity: WARNING input_case: exact diff --git a/mobsfscan/rules/semgrep/webview/webview_mixed_content.yaml b/mobsfscan/rules/semgrep/webview/webview_mixed_content.yaml new file mode 100644 index 0000000..7979394 --- /dev/null +++ b/mobsfscan/rules/semgrep/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/tests/assets/rules/semgrep/webview/webview_mixed_content.java b/tests/assets/rules/semgrep/webview/webview_mixed_content.java new file mode 100644 index 0000000..a716592 --- /dev/null +++ b/tests/assets/rules/semgrep/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); + } +} From c4e941b7c39c39381094d3fe316b833875af14c8 Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 19:28:04 -0700 Subject: [PATCH 02/19] Improve SARIF rule titles and dashboard metadata. Use descriptive rule names plus security-severity, tags, help text, and defaultConfiguration so GitLab/GitHub/SonarQube show meaningful findings. Fixes https://github.com/MobSF/mobsfscan/issues/116 Co-authored-by: Cursor --- mobsfscan/formatters/sarif.py | 158 ++++++++++++++++++++++++++++++---- tests/unit/test_sarif.py | 84 ++++++++++++++++++ 2 files changed, 223 insertions(+), 19 deletions(-) create mode 100644 tests/unit/test_sarif.py diff --git a/mobsfscan/formatters/sarif.py b/mobsfscan/formatters/sarif.py index 4d237f8..e9f2087 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,116 @@ 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) + if 0.1 <= score <= 10.0: + return f'{score:.1f}' + except (TypeError, ValueError): + pass + 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 +161,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 +195,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 +221,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/tests/unit/test_sarif.py b/tests/unit/test_sarif.py new file mode 100644 index 0000000..42eae5a --- /dev/null +++ b/tests/unit/test_sarif.py @@ -0,0 +1,84 @@ +# -*- coding: utf_8 -*- +"""Tests for SARIF rule naming and dashboard metadata.""" +from mobsfscan.formatters.sarif import ( + build_tags, + format_rule_name, + sarif_output, + security_severity_score, +) +import json + + +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, '0.4.6', ['app']) + out = json.loads(outfile.read_text()) + rule = out['runs'][0]['tool']['driver']['rules'][0] + result = out['runs'][0]['results'][0] + + 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'] From f2e7b8fa43aa70c8dfa1721bdee4a302ebbb0085 Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 19:33:17 -0700 Subject: [PATCH 03/19] Add native GitLab SAST report output. Introduce --gitlab-sast to emit GitLab's SAST JSON directly, with CWE/OWASP/MASVS mapping and README CI examples. Fixes https://github.com/MobSF/mobsfscan/issues/115 Co-authored-by: Cursor --- README.md | 32 +++-- action.yml | 1 + mobsfscan/__main__.py | 15 +- mobsfscan/formatters/gitlab_sast.py | 205 ++++++++++++++++++++++++++++ tests/unit/test_gitlab_sast.py | 104 ++++++++++++++ 5 files changed, 347 insertions(+), 10 deletions(-) create mode 100644 mobsfscan/formatters/gitlab_sast.py create mode 100644 tests/unit/test_gitlab_sast.py diff --git a/README.md b/README.md index c9bb97c..0e3b150 100644 --- a/README.md +++ b/README.md @@ -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: @@ -43,6 +44,7 @@ options: --json set output format as JSON --sarif set output format as SARIF 2.1.0 --sonarqube set output format compatible with SonarQube + --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 @@ -295,15 +297,27 @@ 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: + +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. #### Travis CI diff --git a/action.yml b/action.yml index 1cd3ff1..c1d3d7c 100644 --- a/action.yml +++ b/action.yml @@ -16,6 +16,7 @@ inputs: --json set output format as JSON --sarif set output format as SARIF 2.1.0 --sonarqube set output format compatible with SonarQube + --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/__main__.py b/mobsfscan/__main__.py index 0d9451a..687912f 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, @@ -44,6 +45,9 @@ def main(): parser.add_argument('--sonarqube', help='set output format compatible with SonarQube', 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', action='store_true') @@ -78,7 +82,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 +99,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/tests/unit/test_gitlab_sast.py b/tests/unit/test_gitlab_sast.py new file mode 100644 index 0000000..95de021 --- /dev/null +++ b/tests/unit/test_gitlab_sast.py @@ -0,0 +1,104 @@ +# -*- coding: utf_8 -*- +"""Tests for GitLab SAST report formatter.""" +import json + +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, '0.4.6') + 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'] == '0.4.6' + 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, '0.4.6') + report = json.loads(outfile.read_text()) + vuln = report['vulnerabilities'][0] + assert vuln['location']['file'] == '.' + assert vuln['location']['start_line'] == 1 From 114c12dd9f8939e461418a34960d95213e3e86d4 Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 19:35:30 -0700 Subject: [PATCH 04/19] Upgrade SonarQube output to the 10.3+ generic issue format. Emit separate rules and issues arrays with Clean Code attributes and SECURITY impacts so imports no longer warn about the deprecated format. Fixes #114 Co-authored-by: Cursor --- README.md | 11 +- action.yml | 2 +- mobsfscan/__main__.py | 3 +- mobsfscan/formatters/sonarqube.py | 170 +++++++++++++++++++++--------- tests/unit/test_sonarqube.py | 104 ++++++++++++++++++ 5 files changed, 235 insertions(+), 55 deletions(-) create mode 100644 tests/unit/test_sonarqube.py diff --git a/README.md b/README.md index 0e3b150..411fd0c 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ 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} @@ -319,6 +319,15 @@ 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 +``` + +Import with `sonar.externalIssuesReportPaths=mobsfscan-sonar.json`. #### Travis CI diff --git a/action.yml b/action.yml index c1d3d7c..c89ab3f 100644 --- a/action.yml +++ b/action.yml @@ -15,7 +15,7 @@ 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} diff --git a/mobsfscan/__main__.py b/mobsfscan/__main__.py index 687912f..7f5e9a7 100644 --- a/mobsfscan/__main__.py +++ b/mobsfscan/__main__.py @@ -43,7 +43,8 @@ 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', diff --git a/mobsfscan/formatters/sonarqube.py b/mobsfscan/formatters/sonarqube.py index 93de845..04e3af2 100644 --- a/mobsfscan/formatters/sonarqube.py +++ b/mobsfscan/formatters/sonarqube.py @@ -1,67 +1,133 @@ # -*- 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, + """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/tests/unit/test_sonarqube.py b/tests/unit/test_sonarqube.py new file mode 100644 index 0000000..f091e7a --- /dev/null +++ b/tests/unit/test_sonarqube.py @@ -0,0 +1,104 @@ +# -*- coding: utf_8 -*- +"""Tests for SonarQube generic issue formatter (10.3+).""" +import json + +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, '0.4.6') + 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': {}}, '0.0.0')) + assert report == {'rules': [], 'issues': []} From c1110340862de1549e043df03a6ea45b12e1672b Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 19:37:32 -0700 Subject: [PATCH 05/19] Exclude Swift print() from ios_log to cut false positives. print writes to stdout, not the system console like NSLog/os_log. Fixes #112 Co-authored-by: Cursor --- mobsfscan/rules/patterns/ios/swift/swift_rules.yaml | 5 +++-- tests/assets/src/swift/swift.swift | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mobsfscan/rules/patterns/ios/swift/swift_rules.yaml b/mobsfscan/rules/patterns/ios/swift/swift_rules.yaml index 021cfd2..a322157 100644 --- a/mobsfscan/rules/patterns/ios/swift/swift_rules.yaml +++ b/mobsfscan/rules/patterns/ios/swift/swift_rules.yaml @@ -24,9 +24,10 @@ 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_log - message: The App logs information. Sensitive information should never be logged. + message: The App logs information to the system console. Sensitive information should never be logged. input_case: exact - pattern: (print|NSLog|os_log|OSLog|os_signpost)\( + # Exclude Swift print(); it writes to stdout, not the system log (#112). + pattern: (NSLog|os_log|OSLog|os_signpost)\( severity: INFO type: Regex metadata: diff --git a/tests/assets/src/swift/swift.swift b/tests/assets/src/swift/swift.swift index 2586392..2e3ed16 100644 --- a/tests/assets/src/swift/swift.swift +++ b/tests/assets/src/swift/swift.swift @@ -1 +1,3 @@ - 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") From 101abd00b7207c9cfb6b8ccfeb2de93486cfbbb0 Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 19:40:19 -0700 Subject: [PATCH 06/19] Tighten hardcoded key matching to reduce false positives. Require a bare key= or common secret-key names instead of any identifier ending in key (e.g. APP_VERSION_KEY / languageKey). Fixes #111 Co-authored-by: Cursor --- .../patterns/android/kotlin/kotlin_rules.yaml | 3 ++- .../ios/objectivec/objective_c_rules.yaml | 3 ++- .../rules/patterns/ios/swift/swift_rules.yaml | 3 ++- tests/assets/src/swift/swift.swift | 11 ++++++++ tests/unit/test_hardcoded_secret.py | 26 +++++++++++++++++++ 5 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_hardcoded_secret.py diff --git a/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml b/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml index 0a98e8d..4a7770b 100644 --- a/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml +++ b/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml @@ -351,8 +351,9 @@ Files may contain hardcoded sensitive information like usernames, passwords, keys etc. input_case: lower + # Avoid matching lookup names ending in Key; same as ios_hardcoded_secret (#111). 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}) + (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})|((? Date: Sun, 9 Aug 2026 19:44:45 -0700 Subject: [PATCH 07/19] Add .mobsf severity-overrides for per-rule severity. Allow projects to bump or lower rule severity (INFO/WARNING/ERROR) before severity-filter, exit codes, and report formatters run. Fixes #108 Co-authored-by: Cursor --- README.md | 6 ++ mobsfscan/formatters/sonarqube.py | 5 +- mobsfscan/mobsfscan.py | 16 +++++ mobsfscan/utils.py | 42 ++++++++++++- tests/assets/src/dot_mobsf/.mobsf | 6 +- tests/unit/test_dotfile.py | 8 +++ tests/unit/test_sarif.py | 3 +- tests/unit/test_severity_overrides.py | 90 +++++++++++++++++++++++++++ 8 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 tests/unit/test_severity_overrides.py diff --git a/README.md b/README.md index 411fd0c..4d71dd6 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,13 @@ 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. diff --git a/mobsfscan/formatters/sonarqube.py b/mobsfscan/formatters/sonarqube.py index 04e3af2..85b479c 100644 --- a/mobsfscan/formatters/sonarqube.py +++ b/mobsfscan/formatters/sonarqube.py @@ -1,7 +1,8 @@ # -*- coding: utf_8 -*- """SonarQube generic issue format (SonarQube 10.3+). -See: https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/importing-external-issues/generic-issue-import-format +See: +https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/importing-external-issues/generic-issue-import-format """ import json @@ -108,7 +109,7 @@ def build_issue(rule_id, issue_dict): def sonarqube_output(outfile, scan_results, version): - """SonarQube generic issues JSON (rules + issues).""" + """Return SonarQube generic issues JSON (rules + issues).""" del version # kept for CLI signature compatibility rules = [] issues = [] diff --git a/mobsfscan/mobsfscan.py b/mobsfscan/mobsfscan.py index 45d821d..6b9dec7 100644 --- a/mobsfscan/mobsfscan.py +++ b/mobsfscan/mobsfscan.py @@ -40,6 +40,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, } @@ -132,6 +133,7 @@ def format_output(self, results) -> dict: self.format_pattern(results.get('xml_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() @@ -221,6 +223,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() diff --git a/mobsfscan/utils.py b/mobsfscan/utils.py index e541000..7f8ddc1 100644 --- a/mobsfscan/utils.py +++ b/mobsfscan/utils.py @@ -21,6 +21,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 +53,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 +69,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 +79,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 +94,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.' 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/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_sarif.py b/tests/unit/test_sarif.py index 42eae5a..cadb665 100644 --- a/tests/unit/test_sarif.py +++ b/tests/unit/test_sarif.py @@ -1,12 +1,13 @@ # -*- coding: utf_8 -*- """Tests for SARIF rule naming and dashboard metadata.""" +import json + from mobsfscan.formatters.sarif import ( build_tags, format_rule_name, sarif_output, security_severity_score, ) -import json def test_format_rule_name_uses_description_and_cwe(): 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' From 62b96d392594e8b119fec763effcbd559edafc17 Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 19:54:03 -0700 Subject: [PATCH 08/19] Fix mobsf-ignore to apply per match across files. Replace file-wide wipe with a line-level filter, parse ignore rule ids as tokens, and check the full match line span so BOL findings can be suppressed. Supersedes the approach in PR #105. Fixes #99 Fixes #104 Fixes #107 Co-authored-by: Cursor --- README.md | 2 +- mobsfscan/mobsfscan.py | 72 +++++++++---------- .../assets/src/dot_mobsf/scan_but_ignore2.kt | 12 ++++ tests/assets/src/swift_ignore/IgnoreLog.swift | 6 ++ tests/unit/test_ignore_comments.py | 40 +++++++++++ 5 files changed, 94 insertions(+), 38 deletions(-) create mode 100644 tests/assets/src/dot_mobsf/scan_but_ignore2.kt create mode 100644 tests/assets/src/swift_ignore/IgnoreLog.swift create mode 100644 tests/unit/test_ignore_comments.py diff --git a/README.md b/README.md index 4d71dd6..d90c823 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ A `.mobsf` file in the root of the source code directory allows you to configure `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: diff --git a/mobsfscan/mobsfscan.py b/mobsfscan/mobsfscan.py index 6b9dec7..43a8d7e 100644 --- a/mobsfscan/mobsfscan.py +++ b/mobsfscan/mobsfscan.py @@ -249,51 +249,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/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/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/unit/test_ignore_comments.py b/tests/unit/test_ignore_comments.py new file mode 100644 index 0000000..27654e7 --- /dev/null +++ b/tests/unit/test_ignore_comments.py @@ -0,0 +1,40 @@ +# -*- 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 + assert sorted(f['match_string'] for f in files) == ['NSLog(', 'os_log('] + for match in files: + assert not scan.suppress_pm_comments(match, 'ios_log') From c1c28efdd934bb78b8cd56d18c5cd463f0b4ae27 Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 20:08:41 -0700 Subject: [PATCH 09/19] Require libsast 3.1.8 for Semgrep ARG_MAX batching. Pull in the Semgrep argv batching fix so large trees no longer fail with "Argument list too long" (mobsfscan#98). Co-authored-by: Cursor --- Pipfile | 2 +- Pipfile.lock | 6 +++--- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) 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..b5cfd42 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -467,12 +467,12 @@ }, "libsast": { "hashes": [ - "sha256:191002e6e9a6b9f81206bec2bf3bdea5c57034459a9537c02748e66a64650db6", - "sha256:6321d6d00a72b39aa7fadc7053ae84bc24681ffe4f6925cc8b6b2bb7c0635ad7" + "sha256:31a26d76079ac2f4635b6b17784867955c01350255e781f9df0c1d587d408d10", + "sha256:8eba9d45697bca51dffee5c8f96118a4ee1646bd87f06416baf07453602a4eeb" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==3.1.7" + "version": "==3.1.8" }, "markdown-it-py": { "hashes": [ diff --git a/requirements.txt b/requirements.txt index 64a8e5c..19e7f95 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ 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 diff --git a/setup.py b/setup.py index 037bbba..a12d5a3 100644 --- a/setup.py +++ b/setup.py @@ -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', From 0f44e999050e05713c87cf34d8b7c5db46e4337d Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 20:10:25 -0700 Subject: [PATCH 10/19] Raise hardcoded-secret string length limit to 8192. Kotlin/iOS regex rules capped values at 100 chars, so long hex keys and similar secrets were false negatives while Java Semgrep was not. Fixes #88 Co-authored-by: Cursor --- .../patterns/android/kotlin/kotlin_rules.yaml | 3 ++- .../ios/objectivec/objective_c_rules.yaml | 3 ++- .../rules/patterns/ios/swift/swift_rules.yaml | 3 ++- tests/assets/src/kotlin_long_secret/LongKey.kt | 2 ++ tests/unit/test_hardcoded_secret.py | 16 +++++++++++++++- 5 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 tests/assets/src/kotlin_long_secret/LongKey.kt diff --git a/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml b/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml index 4a7770b..5a23ce7 100644 --- a/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml +++ b/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml @@ -352,8 +352,9 @@ passwords, keys etc. input_case: lower # Avoid matching lookup names ending in Key; same as ios_hardcoded_secret (#111). + # Allow long literals (hex keys/PEMs); .{1,100} caused false negatives (#88). 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})|((? 100 for m in matches) From 789bb965a638f9af5bc6dc425eb701f953bf8fa1 Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 20:13:21 -0700 Subject: [PATCH 11/19] Fix network_security_config crash with multiple domain-config blocks. xmltodict returns a list for sibling domain-config nodes; iterate configs (including nested) instead of calling .get on the list. Fixes #87 Co-authored-by: Cursor --- mobsfscan/manifest.py | 61 +++++++++++-------- .../nsc_multiple_domain_config_siblings.xml | 13 ++++ tests/unit/test_xml.py | 13 ++++ 3 files changed, 61 insertions(+), 26 deletions(-) create mode 100644 tests/assets/src/xml/nsc_multiple_domain_config_siblings.xml diff --git a/mobsfscan/manifest.py b/mobsfscan/manifest.py index e5b540c..f86f348 100644 --- a/mobsfscan/manifest.py +++ b/mobsfscan/manifest.py @@ -249,7 +249,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 +261,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/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/test_xml.py b/tests/unit/test_xml.py index df00a75..98a3520 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,12 @@ 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) From da7f58da8a25a967887ed814214818f10686c15e Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 20:21:54 -0700 Subject: [PATCH 12/19] Add iOS Info.plist App Transport Security checks. Port MobSF ATS analysis so NSAllowsArbitraryLoads and exception-domain misconfigurations are reported from Info.plist during iOS/auto scans. Co-authored-by: Cursor --- README.md | 2 +- mobsfscan/ios_plist.py | 138 ++++++++++++++++++++++++++ mobsfscan/manifest_metadata.py | 133 +++++++++++++++++++++++++ mobsfscan/mobsfscan.py | 24 +++++ tests/assets/src/ios_plist/Info.plist | 44 ++++++++ tests/unit/test_ios_plist.py | 53 ++++++++++ 6 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 mobsfscan/ios_plist.py create mode 100644 tests/assets/src/ios_plist/Info.plist create mode 100644 tests/unit/test_ios_plist.py diff --git a/README.md b/README.md index d90c823..a06d61e 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) diff --git a/mobsfscan/ios_plist.py b/mobsfscan/ios_plist.py new file mode 100644 index 0000000..04dcb7a --- /dev/null +++ b/mobsfscan/ios_plist.py @@ -0,0 +1,138 @@ +# -*- 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 + + +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( + plist_path.resolve().as_posix(), + 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_metadata.py b/mobsfscan/manifest_metadata.py index a251373..c0b2f83 100644 --- a/mobsfscan/manifest_metadata.py +++ b/mobsfscan/manifest_metadata.py @@ -319,4 +319,137 @@ 'Communication.md'), }, }, + # 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 43a8d7e..5508fea 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, @@ -50,10 +51,12 @@ def __init__( 'errors': [], } self.xmls = [] + self.plists = [] self.best_practices = None self.standards = standards.get_standards() self.get_extensions() self.get_xmls() + self.get_plists() def rules_selector(self, suffix): """Get rule extensions from suffix.""" @@ -106,6 +109,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) @@ -120,6 +133,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) @@ -131,6 +154,7 @@ def format_output(self, results) -> dict: # 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() 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/unit/test_ios_plist.py b/tests/unit/test_ios_plist.py new file mode 100644 index 0000000..4a94871 --- /dev/null +++ b/tests/unit/test_ios_plist.py @@ -0,0 +1,53 @@ +# -*- 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'] + + +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 == [] From 63186fa63d29e36262396ea7ff3d42cb354ae080 Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 20:41:21 -0700 Subject: [PATCH 13/19] Add Android security rules researched from Minded Security MASTG Semgrep. Clean-room Semgrep, Kotlin, and layout XML checks cover biometric crypto binding, weak TLS, sensitive input caching, custom XOR crypto, and sensitive notifications without importing GPL rule text. Also refresh README CI action versions. Fixes #68. Co-authored-by: Cursor --- README.md | 17 +++-- mobsfscan/manifest.py | 46 +++++++++++- mobsfscan/manifest_metadata.py | 17 +++++ .../patterns/android/kotlin/kotlin_rules.yaml | 74 +++++++++++++++++++ .../semgrep/android/biometric_crypto.yaml | 33 +++++++++ .../semgrep/android/sensitive_input.yaml | 27 +++++++ .../android/sensitive_notification.yaml | 27 +++++++ .../semgrep/crypto/custom_xor_crypto.yaml | 29 ++++++++ .../network/weak_tls_configuration.yaml | 44 +++++++++++ .../semgrep/android/biometric_crypto.java | 18 +++++ .../semgrep/android/sensitive_input.java | 14 ++++ .../android/sensitive_notification.java | 13 ++++ .../semgrep/crypto/custom_xor_crypto.java | 18 +++++ .../network/weak_tls_configuration.java | 23 ++++++ .../assets/src/android_layout/safe_login.xml | 12 +++ .../src/android_layout/unsafe_login.xml | 12 +++ .../assets/src/android_new_rules/NewRules.kt | 25 +++++++ tests/unit/setup_test.py | 4 + tests/unit/test_matcher.py | 13 ++++ tests/unit/test_xml.py | 9 +++ 20 files changed, 467 insertions(+), 8 deletions(-) create mode 100644 mobsfscan/rules/semgrep/android/biometric_crypto.yaml create mode 100644 mobsfscan/rules/semgrep/android/sensitive_input.yaml create mode 100644 mobsfscan/rules/semgrep/android/sensitive_notification.yaml create mode 100644 mobsfscan/rules/semgrep/crypto/custom_xor_crypto.yaml create mode 100644 mobsfscan/rules/semgrep/network/weak_tls_configuration.yaml create mode 100644 tests/assets/rules/semgrep/android/biometric_crypto.java create mode 100644 tests/assets/rules/semgrep/android/sensitive_input.java create mode 100644 tests/assets/rules/semgrep/android/sensitive_notification.java create mode 100644 tests/assets/rules/semgrep/crypto/custom_xor_crypto.java create mode 100644 tests/assets/rules/semgrep/network/weak_tls_configuration.java create mode 100644 tests/assets/src/android_layout/safe_login.xml create mode 100644 tests/assets/src/android_layout/unsafe_login.xml create mode 100644 tests/assets/src/android_new_rules/NewRules.kt diff --git a/README.md b/README.md index a06d61e..7f82ca0 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,7 @@ A `.mobsf` file in the root of the source code directory allows you to configure ``` `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` on the line that triggers the finding. Only that match is suppressed; other matches of the same rule in the file still report. @@ -254,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 @@ -280,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 @@ -291,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 ``` @@ -356,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/mobsfscan/manifest.py b/mobsfscan/manifest.py index f86f348..e8437b4 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 @@ -20,6 +21,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', @@ -114,7 +119,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 +151,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]) diff --git a/mobsfscan/manifest_metadata.py b/mobsfscan/manifest_metadata.py index c0b2f83..262b7c4 100644 --- a/mobsfscan/manifest_metadata.py +++ b/mobsfscan/manifest_metadata.py @@ -319,6 +319,23 @@ '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': ( diff --git a/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml b/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml index 5a23ce7..aba2b08 100644 --- a/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml +++ b/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml @@ -362,3 +362,77 @@ 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 +- id: android_kotlin_insecure_tls_version + 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. + type: RegexOr + pattern: + - 'SSLContext\.getInstance\(\s*"TLSv1(?:\.0|\.1)?"' + - 'setEnabledProtocols\([^)]{0,512}"TLSv1(?:\.0|\.1)?"' + input_case: exact + 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 + message: >- + The explicitly enabled TLS cipher suites include a null, anonymous, + export-grade, RC4, DES/3DES, or MD5-based suite. + type: Regex + pattern: >- + (?i)setEnabledCipherSuites\([^)]{0,512}(?:_NULL_|_ANON_|_EXPORT_|_RC4_|_DES_|3DES|_MD5) + input_case: exact + 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/ +- id: android_kotlin_sensitive_input_keyboard_cache + message: >- + A sensitive input field is configured without a password variation or + TYPE_TEXT_FLAG_NO_SUGGESTIONS. Disable suggestions for sensitive input. + type: RegexOr + pattern: + - >- + (?i)(?:password|passcode|pin|secret|otp|token)\w*\.setInputType\((?![^)]*(?:TYPE_TEXT_VARIATION_PASSWORD|TYPE_TEXT_VARIATION_VISIBLE_PASSWORD|TYPE_NUMBER_VARIATION_PASSWORD|TYPE_TEXT_FLAG_NO_SUGGESTIONS))[^)]*\) + - >- + (?i)(?:password|passcode|pin|secret|otp|token)\w*\.inputType\s*=\s*(?![^\n]*(?:TYPE_TEXT_VARIATION_PASSWORD|TYPE_TEXT_VARIATION_VISIBLE_PASSWORD|TYPE_NUMBER_VARIATION_PASSWORD|TYPE_TEXT_FLAG_NO_SUGGESTIONS))[^\n]+ + input_case: exact + 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_custom_xor_crypto + message: >- + A cryptography-named function uses XOR directly. Use a standard + authenticated-encryption construction instead of custom cryptography. + type: Regex + pattern: >- + (?i)fun\s+\w*(?:encrypt|decrypt|crypt)\w*\s*\([^)]*\)[^\n]{0,512}(?:\s+xor\s+|\.xor\() + input_case: exact + 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_sensitive_notification + message: >- + Secret-like data is displayed in a notification and may be exposed on the + lock screen or to notification listeners. + type: Regex + pattern: >- + (?i)\.set(?:ContentText|ContentTitle|SubText|Ticker)\(\s*\w*(?:password|passcode|pin|secret|otp|token|auth(?:entication)?code)\w* + input_case: exact + 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/biometric_crypto.yaml b/mobsfscan/rules/semgrep/android/biometric_crypto.yaml new file mode 100644 index 0000000..d152339 --- /dev/null +++ b/mobsfscan/rules/semgrep/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/sensitive_input.yaml b/mobsfscan/rules/semgrep/android/sensitive_input.yaml new file mode 100644 index 0000000..ae801bf --- /dev/null +++ b/mobsfscan/rules/semgrep/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/android/sensitive_notification.yaml b/mobsfscan/rules/semgrep/android/sensitive_notification.yaml new file mode 100644 index 0000000..470f7c7 --- /dev/null +++ b/mobsfscan/rules/semgrep/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/crypto/custom_xor_crypto.yaml b/mobsfscan/rules/semgrep/crypto/custom_xor_crypto.yaml new file mode 100644 index 0000000..16dc8d4 --- /dev/null +++ b/mobsfscan/rules/semgrep/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/network/weak_tls_configuration.yaml b/mobsfscan/rules/semgrep/network/weak_tls_configuration.yaml new file mode 100644 index 0000000..b790240 --- /dev/null +++ b/mobsfscan/rules/semgrep/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/tests/assets/rules/semgrep/android/biometric_crypto.java b/tests/assets/rules/semgrep/android/biometric_crypto.java new file mode 100644 index 0000000..ef3cdc3 --- /dev/null +++ b/tests/assets/rules/semgrep/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/sensitive_input.java b/tests/assets/rules/semgrep/android/sensitive_input.java new file mode 100644 index 0000000..cd2279f --- /dev/null +++ b/tests/assets/rules/semgrep/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/android/sensitive_notification.java b/tests/assets/rules/semgrep/android/sensitive_notification.java new file mode 100644 index 0000000..7e19c8e --- /dev/null +++ b/tests/assets/rules/semgrep/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/crypto/custom_xor_crypto.java b/tests/assets/rules/semgrep/crypto/custom_xor_crypto.java new file mode 100644 index 0000000..acdb47b --- /dev/null +++ b/tests/assets/rules/semgrep/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/network/weak_tls_configuration.java b/tests/assets/rules/semgrep/network/weak_tls_configuration.java new file mode 100644 index 0000000..bb75501 --- /dev/null +++ b/tests/assets/rules/semgrep/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/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/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/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_matcher.py b/tests/unit/test_matcher.py index 9f8ab3f..8202ec3 100644 --- a/tests/unit/test_matcher.py +++ b/tests/unit/test_matcher.py @@ -11,6 +11,19 @@ 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', + } + assert expected.issubset(res['results']) + + def test_ios(): paths = get_paths() diff --git a/tests/unit/test_xml.py b/tests/unit/test_xml.py index 98a3520..70f295d 100644 --- a/tests/unit/test_xml.py +++ b/tests/unit/test_xml.py @@ -22,3 +22,12 @@ def test_multiple_sibling_domain_configs(): 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') From 9a61e0a4968a888d1096c26aadb046df9d4c8f31 Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 21:11:28 -0700 Subject: [PATCH 14/19] Migrate Kotlin and Swift rules to Semgrep and organize by language. Replace Android Kotlin and iOS Swift regex patterns with Semgrep rules, move Java Semgrep under java/, keep Objective-C on libsast, and preserve best-practice missing-control inversion per language. Co-authored-by: Cursor --- mobsfscan/__init__.py | 2 +- mobsfscan/mobsfscan.py | 16 +- .../android/kotlin/best_practices.yaml | 103 ---- .../patterns/android/kotlin/kotlin_rules.yaml | 438 ------------------ .../patterns/ios/swift/best_practices.yaml | 116 ----- .../rules/patterns/ios/swift/swift_rules.yaml | 222 --------- .../{ => java}/android_safetynetapi.yaml | 0 .../{ => java}/flag_secure.yaml | 0 .../{ => java}/root_detection.yaml | 0 .../best_practices/{ => java}/tapjacking.yaml | 0 .../tls_certificate_transparency.yaml | 0 .../{ => java}/tls_pinning.yaml | 0 .../kotlin/android_safetynet.yaml | 25 + .../best_practices/kotlin/flag_secure.yaml | 30 ++ .../best_practices/kotlin/root_detection.yaml | 27 ++ .../best_practices/kotlin/tapjacking.yaml | 15 + .../kotlin/tls_certificate_transparency.yaml | 22 + .../best_practices/kotlin/tls_pinning.yaml | 37 ++ .../best_practices/swift/jailbreak.yaml | 14 + .../best_practices/swift/keyboard.yaml | 34 ++ .../best_practices/swift/resilience.yaml | 29 ++ .../{ => java}/android/biometric_crypto.yaml | 0 .../semgrep/{ => java}/android/hidden_ui.yaml | 0 .../semgrep/{ => java}/android/logging.yaml | 0 .../semgrep/{ => java}/android/secrets.yaml | 0 .../{ => java}/android/sensitive_input.yaml | 0 .../android/sensitive_notification.yaml | 0 .../android/word_readable_writable.yaml | 0 .../semgrep/{ => java}/crypto/aes_ecb.yaml | 0 .../crypto/aes_encryption_keys.yaml | 0 .../{ => java}/crypto/cbc_padding_oracle.yaml | 0 .../{ => java}/crypto/cbc_static_iv.yaml | 0 .../{ => java}/crypto/custom_xor_crypto.yaml | 0 .../{ => java}/crypto/insecure_random.yaml | 0 .../{ => java}/crypto/insecure_ssl_v3.yaml | 0 .../{ => java}/crypto/rsa_no_oeap.yaml | 0 .../semgrep/{ => java}/crypto/sha1_hash.yaml | 0 .../{ => java}/crypto/weak_ciphers.yaml | 0 .../{ => java}/crypto/weak_hashes.yaml | 0 .../semgrep/{ => java}/crypto/weak_iv.yaml | 0 .../{ => java}/crypto/weak_key_size.yaml | 0 .../jackson_deserialization.yaml | 0 .../object_deserialization.yaml | 0 .../injection/command_injection.yaml | 0 .../injection/command_injection_formated.yaml | 0 .../injection/sqlite_injection.yaml | 0 .../network/accept_self_signed.yaml | 0 .../network/default_http_client_tls.yaml | 0 .../network/weak_tls_configuration.yaml | 0 .../webview/webview_allow_file_from_url.yaml | 0 .../{ => java}/webview/webview_debugging.yaml | 0 .../webview/webview_external_storage.yaml | 0 .../webview/webview_file_access.yaml | 0 .../webview/webview_ignore_ssl_errors.yaml | 0 .../webview/webview_javascript_interface.yaml | 0 .../webview/webview_mixed_content.yaml | 0 .../{ => java}/xxe/xmldecoder_xxe.yaml | 0 .../xmlfactory_external_entities_enabled.yaml | 0 .../{ => java}/xxe/xmlfactory_xxe.yaml | 0 mobsfscan/rules/semgrep/kotlin/android.yaml | 186 ++++++++ mobsfscan/rules/semgrep/kotlin/biometric.yaml | 37 ++ mobsfscan/rules/semgrep/kotlin/crypto.yaml | 330 +++++++++++++ mobsfscan/rules/semgrep/kotlin/injection.yaml | 189 ++++++++ mobsfscan/rules/semgrep/kotlin/network.yaml | 77 +++ mobsfscan/rules/semgrep/kotlin/webview.yaml | 147 ++++++ mobsfscan/rules/semgrep/swift/auth.yaml | 59 +++ mobsfscan/rules/semgrep/swift/crypto.yaml | 66 +++ mobsfscan/rules/semgrep/swift/logging.yaml | 23 + mobsfscan/rules/semgrep/swift/network.yaml | 55 +++ mobsfscan/rules/semgrep/swift/secrets.yaml | 33 ++ mobsfscan/rules/semgrep/swift/storage.yaml | 29 ++ mobsfscan/rules/semgrep/swift/webview.yaml | 32 ++ mobsfscan/settings.py | 3 - mobsfscan/utils.py | 39 +- .../{ => java}/android_safetynetapi.java | 0 .../{ => java}/flag_secure.java | 0 .../{ => java}/root_detection.java | 0 .../best_practices/{ => java}/tapjacking.java | 0 .../tls_certificate_transparency.java | 0 .../{ => java}/tls_pinning.java | 0 .../kotlin/android_safetynet.kt | 7 + .../best_practices/kotlin/flag_secure.kt | 10 + .../best_practices/kotlin/root_detection.kt | 11 + .../best_practices/kotlin/tapjacking.kt | 5 + .../kotlin/tls_certificate_transparency.kt | 7 + .../best_practices/kotlin/tls_pinning.kt | 7 + .../best_practices/swift/jailbreak.swift | 5 + .../best_practices/swift/keyboard.swift | 9 + .../best_practices/swift/resilience.swift | 9 + .../{ => java}/android/biometric_crypto.java | 0 .../semgrep/{ => java}/android/hidden_ui.java | 0 .../semgrep/{ => java}/android/logging.java | 0 .../semgrep/{ => java}/android/secrets.java | 0 .../{ => java}/android/sensitive_input.java | 0 .../android/sensitive_notification.java | 0 .../android/word_readable_writable.java | 0 .../semgrep/{ => java}/crypto/aes_ecb.java | 0 .../crypto/aes_encryption_keys.java | 0 .../{ => java}/crypto/cbc_padding_oracle.java | 0 .../{ => java}/crypto/cbc_static_iv.java | 0 .../{ => java}/crypto/custom_xor_crypto.java | 0 .../{ => java}/crypto/insecure_random.java | 0 .../{ => java}/crypto/insecure_ssl_v3.java | 0 .../{ => java}/crypto/rsa_no_oeap.java | 0 .../semgrep/{ => java}/crypto/sha1_hash.java | 0 .../{ => java}/crypto/weak_ciphers.java | 0 .../{ => java}/crypto/weak_hashes.java | 0 .../semgrep/{ => java}/crypto/weak_iv.java | 0 .../{ => java}/crypto/weak_key_size.java | 0 .../jackson_deserialization.java | 0 .../object_deserialization.java | 0 .../injection/command_injection.java | 0 .../injection/command_injection_formated.java | 0 .../injection/sqlite_injection.java | 0 .../network/accept_self_signed.java | 0 .../network/default_http_client.tls.java | 0 .../network/weak_tls_configuration.java | 0 .../webview/webview_allow_file_from_url.java | 0 .../{ => java}/webview/webview_debugging.java | 0 .../webview/webview_external_storage.java | 0 .../webview/webview_file_access.java | 0 .../webview/webview_ignore_ssl_errors.java | 0 .../webview/webview_javascript_interface.java | 0 .../webview/webview_mixed_content.java | 0 .../{ => java}/xxe/xmldecoder_xxe.java | 0 .../xmlfactory_external_entities_enabled.java | 0 .../{ => java}/xxe/xmlfactory_xxe.java | 0 tests/assets/rules/semgrep/kotlin/android.kt | 54 +++ .../assets/rules/semgrep/kotlin/biometric.kt | 15 + tests/assets/rules/semgrep/kotlin/crypto.kt | 84 ++++ .../assets/rules/semgrep/kotlin/injection.kt | 66 +++ tests/assets/rules/semgrep/kotlin/network.kt | 15 + tests/assets/rules/semgrep/kotlin/webview.kt | 35 ++ tests/assets/rules/semgrep/swift/auth.swift | 11 + tests/assets/rules/semgrep/swift/crypto.swift | 13 + .../assets/rules/semgrep/swift/logging.swift | 7 + .../assets/rules/semgrep/swift/network.swift | 11 + .../assets/rules/semgrep/swift/secrets.swift | 14 + .../assets/rules/semgrep/swift/storage.swift | 5 + .../assets/rules/semgrep/swift/webview.swift | 7 + .../assets/src/android_new_rules/JavaPorts.kt | 66 +++ .../ControlsPresent.java | 16 + .../ControlsPresent.kt | 13 + .../ControlsPresent.swift | 8 + tests/unit/test_hardcoded_secret.py | 34 +- tests/unit/test_ignore_comments.py | 6 +- tests/unit/test_java_best_practices.py | 49 ++ tests/unit/test_kotlin_best_practices.py | 48 ++ tests/unit/test_matcher.py | 11 + tests/unit/test_swift_best_practices.py | 45 ++ 150 files changed, 2220 insertions(+), 918 deletions(-) delete mode 100644 mobsfscan/rules/patterns/android/kotlin/best_practices.yaml delete mode 100644 mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml delete mode 100644 mobsfscan/rules/patterns/ios/swift/best_practices.yaml delete mode 100644 mobsfscan/rules/patterns/ios/swift/swift_rules.yaml rename mobsfscan/rules/semgrep/best_practices/{ => java}/android_safetynetapi.yaml (100%) rename mobsfscan/rules/semgrep/best_practices/{ => java}/flag_secure.yaml (100%) rename mobsfscan/rules/semgrep/best_practices/{ => java}/root_detection.yaml (100%) rename mobsfscan/rules/semgrep/best_practices/{ => java}/tapjacking.yaml (100%) rename mobsfscan/rules/semgrep/best_practices/{ => java}/tls_certificate_transparency.yaml (100%) rename mobsfscan/rules/semgrep/best_practices/{ => java}/tls_pinning.yaml (100%) create mode 100644 mobsfscan/rules/semgrep/best_practices/kotlin/android_safetynet.yaml create mode 100644 mobsfscan/rules/semgrep/best_practices/kotlin/flag_secure.yaml create mode 100644 mobsfscan/rules/semgrep/best_practices/kotlin/root_detection.yaml create mode 100644 mobsfscan/rules/semgrep/best_practices/kotlin/tapjacking.yaml create mode 100644 mobsfscan/rules/semgrep/best_practices/kotlin/tls_certificate_transparency.yaml create mode 100644 mobsfscan/rules/semgrep/best_practices/kotlin/tls_pinning.yaml create mode 100644 mobsfscan/rules/semgrep/best_practices/swift/jailbreak.yaml create mode 100644 mobsfscan/rules/semgrep/best_practices/swift/keyboard.yaml create mode 100644 mobsfscan/rules/semgrep/best_practices/swift/resilience.yaml rename mobsfscan/rules/semgrep/{ => java}/android/biometric_crypto.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/android/hidden_ui.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/android/logging.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/android/secrets.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/android/sensitive_input.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/android/sensitive_notification.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/android/word_readable_writable.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/aes_ecb.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/aes_encryption_keys.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/cbc_padding_oracle.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/cbc_static_iv.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/custom_xor_crypto.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/insecure_random.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/insecure_ssl_v3.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/rsa_no_oeap.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/sha1_hash.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/weak_ciphers.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/weak_hashes.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/weak_iv.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/crypto/weak_key_size.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/deserialization/jackson_deserialization.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/deserialization/object_deserialization.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/injection/command_injection.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/injection/command_injection_formated.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/injection/sqlite_injection.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/network/accept_self_signed.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/network/default_http_client_tls.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/network/weak_tls_configuration.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/webview/webview_allow_file_from_url.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/webview/webview_debugging.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/webview/webview_external_storage.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/webview/webview_file_access.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/webview/webview_ignore_ssl_errors.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/webview/webview_javascript_interface.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/webview/webview_mixed_content.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/xxe/xmldecoder_xxe.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/xxe/xmlfactory_external_entities_enabled.yaml (100%) rename mobsfscan/rules/semgrep/{ => java}/xxe/xmlfactory_xxe.yaml (100%) create mode 100644 mobsfscan/rules/semgrep/kotlin/android.yaml create mode 100644 mobsfscan/rules/semgrep/kotlin/biometric.yaml create mode 100644 mobsfscan/rules/semgrep/kotlin/crypto.yaml create mode 100644 mobsfscan/rules/semgrep/kotlin/injection.yaml create mode 100644 mobsfscan/rules/semgrep/kotlin/network.yaml create mode 100644 mobsfscan/rules/semgrep/kotlin/webview.yaml create mode 100644 mobsfscan/rules/semgrep/swift/auth.yaml create mode 100644 mobsfscan/rules/semgrep/swift/crypto.yaml create mode 100644 mobsfscan/rules/semgrep/swift/logging.yaml create mode 100644 mobsfscan/rules/semgrep/swift/network.yaml create mode 100644 mobsfscan/rules/semgrep/swift/secrets.yaml create mode 100644 mobsfscan/rules/semgrep/swift/storage.yaml create mode 100644 mobsfscan/rules/semgrep/swift/webview.yaml rename tests/assets/rules/semgrep/best_practices/{ => java}/android_safetynetapi.java (100%) rename tests/assets/rules/semgrep/best_practices/{ => java}/flag_secure.java (100%) rename tests/assets/rules/semgrep/best_practices/{ => java}/root_detection.java (100%) rename tests/assets/rules/semgrep/best_practices/{ => java}/tapjacking.java (100%) rename tests/assets/rules/semgrep/best_practices/{ => java}/tls_certificate_transparency.java (100%) rename tests/assets/rules/semgrep/best_practices/{ => java}/tls_pinning.java (100%) create mode 100644 tests/assets/rules/semgrep/best_practices/kotlin/android_safetynet.kt create mode 100644 tests/assets/rules/semgrep/best_practices/kotlin/flag_secure.kt create mode 100644 tests/assets/rules/semgrep/best_practices/kotlin/root_detection.kt create mode 100644 tests/assets/rules/semgrep/best_practices/kotlin/tapjacking.kt create mode 100644 tests/assets/rules/semgrep/best_practices/kotlin/tls_certificate_transparency.kt create mode 100644 tests/assets/rules/semgrep/best_practices/kotlin/tls_pinning.kt create mode 100644 tests/assets/rules/semgrep/best_practices/swift/jailbreak.swift create mode 100644 tests/assets/rules/semgrep/best_practices/swift/keyboard.swift create mode 100644 tests/assets/rules/semgrep/best_practices/swift/resilience.swift rename tests/assets/rules/semgrep/{ => java}/android/biometric_crypto.java (100%) rename tests/assets/rules/semgrep/{ => java}/android/hidden_ui.java (100%) rename tests/assets/rules/semgrep/{ => java}/android/logging.java (100%) rename tests/assets/rules/semgrep/{ => java}/android/secrets.java (100%) rename tests/assets/rules/semgrep/{ => java}/android/sensitive_input.java (100%) rename tests/assets/rules/semgrep/{ => java}/android/sensitive_notification.java (100%) rename tests/assets/rules/semgrep/{ => java}/android/word_readable_writable.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/aes_ecb.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/aes_encryption_keys.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/cbc_padding_oracle.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/cbc_static_iv.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/custom_xor_crypto.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/insecure_random.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/insecure_ssl_v3.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/rsa_no_oeap.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/sha1_hash.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/weak_ciphers.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/weak_hashes.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/weak_iv.java (100%) rename tests/assets/rules/semgrep/{ => java}/crypto/weak_key_size.java (100%) rename tests/assets/rules/semgrep/{ => java}/deserialization/jackson_deserialization.java (100%) rename tests/assets/rules/semgrep/{ => java}/deserialization/object_deserialization.java (100%) rename tests/assets/rules/semgrep/{ => java}/injection/command_injection.java (100%) rename tests/assets/rules/semgrep/{ => java}/injection/command_injection_formated.java (100%) rename tests/assets/rules/semgrep/{ => java}/injection/sqlite_injection.java (100%) rename tests/assets/rules/semgrep/{ => java}/network/accept_self_signed.java (100%) rename tests/assets/rules/semgrep/{ => java}/network/default_http_client.tls.java (100%) rename tests/assets/rules/semgrep/{ => java}/network/weak_tls_configuration.java (100%) rename tests/assets/rules/semgrep/{ => java}/webview/webview_allow_file_from_url.java (100%) rename tests/assets/rules/semgrep/{ => java}/webview/webview_debugging.java (100%) rename tests/assets/rules/semgrep/{ => java}/webview/webview_external_storage.java (100%) rename tests/assets/rules/semgrep/{ => java}/webview/webview_file_access.java (100%) rename tests/assets/rules/semgrep/{ => java}/webview/webview_ignore_ssl_errors.java (100%) rename tests/assets/rules/semgrep/{ => java}/webview/webview_javascript_interface.java (100%) rename tests/assets/rules/semgrep/{ => java}/webview/webview_mixed_content.java (100%) rename tests/assets/rules/semgrep/{ => java}/xxe/xmldecoder_xxe.java (100%) rename tests/assets/rules/semgrep/{ => java}/xxe/xmlfactory_external_entities_enabled.java (100%) rename tests/assets/rules/semgrep/{ => java}/xxe/xmlfactory_xxe.java (100%) create mode 100644 tests/assets/rules/semgrep/kotlin/android.kt create mode 100644 tests/assets/rules/semgrep/kotlin/biometric.kt create mode 100644 tests/assets/rules/semgrep/kotlin/crypto.kt create mode 100644 tests/assets/rules/semgrep/kotlin/injection.kt create mode 100644 tests/assets/rules/semgrep/kotlin/network.kt create mode 100644 tests/assets/rules/semgrep/kotlin/webview.kt create mode 100644 tests/assets/rules/semgrep/swift/auth.swift create mode 100644 tests/assets/rules/semgrep/swift/crypto.swift create mode 100644 tests/assets/rules/semgrep/swift/logging.swift create mode 100644 tests/assets/rules/semgrep/swift/network.swift create mode 100644 tests/assets/rules/semgrep/swift/secrets.swift create mode 100644 tests/assets/rules/semgrep/swift/storage.swift create mode 100644 tests/assets/rules/semgrep/swift/webview.swift create mode 100644 tests/assets/src/android_new_rules/JavaPorts.kt create mode 100644 tests/assets/src/java_best_practices_present/ControlsPresent.java create mode 100644 tests/assets/src/kotlin_best_practices_present/ControlsPresent.kt create mode 100644 tests/assets/src/swift_best_practices_present/ControlsPresent.swift create mode 100644 tests/unit/test_java_best_practices.py create mode 100644 tests/unit/test_kotlin_best_practices.py create mode 100644 tests/unit/test_swift_best_practices.py diff --git a/mobsfscan/__init__.py b/mobsfscan/__init__.py index c509bfa..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.6' +__version__ = '1.0.0' __version_info__ = tuple(int(i) for i in __version__.split('.')) __all__ = [ '__title__', diff --git a/mobsfscan/mobsfscan.py b/mobsfscan/mobsfscan.py index 5508fea..505ca3d 100644 --- a/mobsfscan/mobsfscan.py +++ b/mobsfscan/mobsfscan.py @@ -70,17 +70,22 @@ def rules_selector(self, suffix): self.best_practices = '.java' else: self.best_practices = '.kt' - self.options['match_rules'] = settings.ANDROID_RULES_DIR.as_posix() + # Android code + best-practice presence checks use Semgrep only. + 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'} - self.options['match_extensions'] = {'.kt'} + self.options['sgrep_extensions'] = {'.java', '.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'} + # Objective-C remains libsast regex; Swift uses Semgrep. + 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'} def get_extensions(self) -> set: """Get extensions to scan.""" @@ -151,7 +156,6 @@ 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')) 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 aba2b08..0000000 --- a/mobsfscan/rules/patterns/android/kotlin/kotlin_rules.yaml +++ /dev/null @@ -1,438 +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_webview_mixed_content - 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. - type: Regex - pattern: setMixedContentMode\(.{0,48}MIXED_CONTENT_ALWAYS_ALLOW - severity: ERROR - input_case: exact - metadata: - cvss: 7.4 - 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_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 - - \.getSharedPreferences\([^)]{0,50}?,\s*2\s*\) - - '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 - - \.getSharedPreferences\([^)]{0,50}?,\s*1\s*\) - - '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 - # Avoid matching lookup names ending in Key; same as ios_hardcoded_secret (#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})|((?- - 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. - type: RegexOr - pattern: - - 'SSLContext\.getInstance\(\s*"TLSv1(?:\.0|\.1)?"' - - 'setEnabledProtocols\([^)]{0,512}"TLSv1(?:\.0|\.1)?"' - input_case: exact - 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 - message: >- - The explicitly enabled TLS cipher suites include a null, anonymous, - export-grade, RC4, DES/3DES, or MD5-based suite. - type: Regex - pattern: >- - (?i)setEnabledCipherSuites\([^)]{0,512}(?:_NULL_|_ANON_|_EXPORT_|_RC4_|_DES_|3DES|_MD5) - input_case: exact - 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/ -- id: android_kotlin_sensitive_input_keyboard_cache - message: >- - A sensitive input field is configured without a password variation or - TYPE_TEXT_FLAG_NO_SUGGESTIONS. Disable suggestions for sensitive input. - type: RegexOr - pattern: - - >- - (?i)(?:password|passcode|pin|secret|otp|token)\w*\.setInputType\((?![^)]*(?:TYPE_TEXT_VARIATION_PASSWORD|TYPE_TEXT_VARIATION_VISIBLE_PASSWORD|TYPE_NUMBER_VARIATION_PASSWORD|TYPE_TEXT_FLAG_NO_SUGGESTIONS))[^)]*\) - - >- - (?i)(?:password|passcode|pin|secret|otp|token)\w*\.inputType\s*=\s*(?![^\n]*(?:TYPE_TEXT_VARIATION_PASSWORD|TYPE_TEXT_VARIATION_VISIBLE_PASSWORD|TYPE_NUMBER_VARIATION_PASSWORD|TYPE_TEXT_FLAG_NO_SUGGESTIONS))[^\n]+ - input_case: exact - 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_custom_xor_crypto - message: >- - A cryptography-named function uses XOR directly. Use a standard - authenticated-encryption construction instead of custom cryptography. - type: Regex - pattern: >- - (?i)fun\s+\w*(?:encrypt|decrypt|crypt)\w*\s*\([^)]*\)[^\n]{0,512}(?:\s+xor\s+|\.xor\() - input_case: exact - 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_sensitive_notification - message: >- - Secret-like data is displayed in a notification and may be exposed on the - lock screen or to notification listeners. - type: Regex - pattern: >- - (?i)\.set(?:ContentText|ContentTitle|SubText|Ticker)\(\s*\w*(?:password|passcode|pin|secret|otp|token|auth(?:entication)?code)\w* - input_case: exact - 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/patterns/ios/swift/best_practices.yaml b/mobsfscan/rules/patterns/ios/swift/best_practices.yaml deleted file mode 100644 index 3a58f14..0000000 --- a/mobsfscan/rules/patterns/ios/swift/best_practices.yaml +++ /dev/null @@ -1,116 +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: ios_jailbreak_detect - message: This app does not have Jailbreak detection capabilities. - input_case: exact - pattern: - - /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 - - /Library/MobileSubstrate/MobileSubstrate\.dylib - - /usr/libexec/cydia/firmware\.sh - - /private/var/cache/apt/ - - /Library/MobileSubstrate/CydiaSubstrate\.dylib - severity: INFO - type: RegexOr - 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 -- id: ios_custom_keyboard_disabled - message: This app does not have custom keyboards disabled. - input_case: exact - pattern: extensionPointIdentifier == .{0,100}\.keyboard - severity: INFO - type: Regex - 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 - message: This app does not disable Keyboard cache. It must be disabled for all sensitive data inputs. - input_case: exact - pattern: - - \.autocorrectionType\s*=\s*\.no - - \.autocorrectionType\s*=\s*\.No - - UITextAutocorrectionTypeNo - - \.disableAutocorrection\s*\(\s*true\s*\) - - \.autocorrectionDisabled\s*\(\s*true\s*\) - severity: INFO - type: RegexOr - 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 -- id: ios_detect_reversing - message: This app does not have Reverse engineering detection capabilities. - input_case: exact - pattern: - - '"FridaGadget"' - - '"cynject"' - - '"libcycript"' - - '"/usr/sbin/frida-server"' - type: RegexAnd - 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 - message: This app does not have Certificate Pinning implemented in code. - input_case: exact - pattern: - - PinnedCertificatesTrustEvaluator - - TrustKit\.initSharedInstance - severity: INFO - type: RegexOr - 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/patterns/ios/swift/swift_rules.yaml b/mobsfscan/rules/patterns/ios/swift/swift_rules.yaml deleted file mode 100644 index 53df5f4..0000000 --- a/mobsfscan/rules/patterns/ios/swift/swift_rules.yaml +++ /dev/null @@ -1,222 +0,0 @@ -- id: ios_hardcoded_secret - message: Files may contain hardcoded sensitive information like usernames, - passwords, keys etc. - input_case: lower - # 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/android/biometric_crypto.yaml b/mobsfscan/rules/semgrep/java/android/biometric_crypto.yaml similarity index 100% rename from mobsfscan/rules/semgrep/android/biometric_crypto.yaml rename to mobsfscan/rules/semgrep/java/android/biometric_crypto.yaml 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/android/sensitive_input.yaml b/mobsfscan/rules/semgrep/java/android/sensitive_input.yaml similarity index 100% rename from mobsfscan/rules/semgrep/android/sensitive_input.yaml rename to mobsfscan/rules/semgrep/java/android/sensitive_input.yaml diff --git a/mobsfscan/rules/semgrep/android/sensitive_notification.yaml b/mobsfscan/rules/semgrep/java/android/sensitive_notification.yaml similarity index 100% rename from mobsfscan/rules/semgrep/android/sensitive_notification.yaml rename to mobsfscan/rules/semgrep/java/android/sensitive_notification.yaml 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/crypto/custom_xor_crypto.yaml b/mobsfscan/rules/semgrep/java/crypto/custom_xor_crypto.yaml similarity index 100% rename from mobsfscan/rules/semgrep/crypto/custom_xor_crypto.yaml rename to mobsfscan/rules/semgrep/java/crypto/custom_xor_crypto.yaml 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/network/weak_tls_configuration.yaml b/mobsfscan/rules/semgrep/java/network/weak_tls_configuration.yaml similarity index 100% rename from mobsfscan/rules/semgrep/network/weak_tls_configuration.yaml rename to mobsfscan/rules/semgrep/java/network/weak_tls_configuration.yaml 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/webview/webview_mixed_content.yaml b/mobsfscan/rules/semgrep/java/webview/webview_mixed_content.yaml similarity index 100% rename from mobsfscan/rules/semgrep/webview/webview_mixed_content.yaml rename to mobsfscan/rules/semgrep/java/webview/webview_mixed_content.yaml 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 7f8ddc1..7d2f17c 100644 --- a/mobsfscan/utils.py +++ b/mobsfscan/utils.py @@ -130,27 +130,38 @@ def read_yaml(file_obj, text=False): def get_best_practices(extension): - """Get best practices of an extension.""" + """Get best practices of an extension. + + Best-practice rules match control *presence*. MobSFScan.missing_controls() + inverts them: delete when present, report when missing across the scan. + """ ids = set() all_rules = {} if extension == '.java': - for yml in config.BEST_PRACTICES_DIR.rglob('*.yaml'): + java_dir = config.BEST_PRACTICES_DIR / 'java' + for yml in java_dir.rglob('*.yaml'): + rules = read_yaml(yml) + for rule in rules['rules']: + all_rules[rule['id']] = rule + ids.add(rule['id']) + elif extension == '.kt': + # Kotlin Semgrep best practices (same inversion as Java). + kt_dir = config.BEST_PRACTICES_DIR / 'kotlin' + for yml in kt_dir.rglob('*.yaml'): + rules = read_yaml(yml) + 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) for rule in rules['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) + elif extension == '.m': + bp = config.IOS_RULES_DIR / 'objectivec' / 'best_practices.yaml' + rules = read_yaml(bp) for rule in rules: all_rules[rule['id']] = rule ids.add(rule['id']) 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/android/biometric_crypto.java b/tests/assets/rules/semgrep/java/android/biometric_crypto.java similarity index 100% rename from tests/assets/rules/semgrep/android/biometric_crypto.java rename to tests/assets/rules/semgrep/java/android/biometric_crypto.java 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/android/sensitive_input.java b/tests/assets/rules/semgrep/java/android/sensitive_input.java similarity index 100% rename from tests/assets/rules/semgrep/android/sensitive_input.java rename to tests/assets/rules/semgrep/java/android/sensitive_input.java diff --git a/tests/assets/rules/semgrep/android/sensitive_notification.java b/tests/assets/rules/semgrep/java/android/sensitive_notification.java similarity index 100% rename from tests/assets/rules/semgrep/android/sensitive_notification.java rename to tests/assets/rules/semgrep/java/android/sensitive_notification.java 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/crypto/custom_xor_crypto.java b/tests/assets/rules/semgrep/java/crypto/custom_xor_crypto.java similarity index 100% rename from tests/assets/rules/semgrep/crypto/custom_xor_crypto.java rename to tests/assets/rules/semgrep/java/crypto/custom_xor_crypto.java 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/network/weak_tls_configuration.java b/tests/assets/rules/semgrep/java/network/weak_tls_configuration.java similarity index 100% rename from tests/assets/rules/semgrep/network/weak_tls_configuration.java rename to tests/assets/rules/semgrep/java/network/weak_tls_configuration.java 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/webview/webview_mixed_content.java b/tests/assets/rules/semgrep/java/webview/webview_mixed_content.java similarity index 100% rename from tests/assets/rules/semgrep/webview/webview_mixed_content.java rename to tests/assets/rules/semgrep/java/webview/webview_mixed_content.java 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_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/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/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/unit/test_hardcoded_secret.py b/tests/unit/test_hardcoded_secret.py index 3008c2c..4b6f008 100644 --- a/tests/unit/test_hardcoded_secret.py +++ b/tests/unit/test_hardcoded_secret.py @@ -10,20 +10,21 @@ def test_ios_hardcoded_secret_skips_lookup_key_names(): res = MobSFScan([str(src)], True, mp='thread').scan() finding = res['results'].get('ios_hardcoded_secret') assert finding is not None - matches = [f.get('match_string') for f in finding.get('files') or []] - joined = ' '.join(matches).lower() + 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() - assert 'app_version_key' not in joined - assert 'languagekey' not in joined - assert 'leadsloggedkey' not in joined + # 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] - assert any('password' in (m or '').lower() for m in matches) - assert any( - m and ( - m.lower().startswith('key') - or 'api_key' in m.lower() - or 'secretkey' in m.lower().replace('_', '')) - for m in matches) + # 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(): @@ -36,5 +37,10 @@ def test_kotlin_long_hardcoded_key_detected(): assert finding is not None matches = finding.get('files') or [] assert matches - assert any('key =' in (m.get('match_string') or '').lower() for m in matches) - assert any(len(m.get('match_string') or '') > 100 for m in 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 index 27654e7..3a1c6c9 100644 --- a/tests/unit/test_ignore_comments.py +++ b/tests/unit/test_ignore_comments.py @@ -35,6 +35,10 @@ def test_ios_log_line_level_and_bol_ignore(): files = res['results']['ios_log']['files'] # Two suppressed NSLog lines removed; unsuppressed NSLog + os_log remain assert len(files) == 2 - assert sorted(f['match_string'] for f in files) == ['NSLog(', 'os_log('] + 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_java_best_practices.py b/tests/unit/test_java_best_practices.py new file mode 100644 index 0000000..8fd8586 --- /dev/null +++ b/tests/unit/test_java_best_practices.py @@ -0,0 +1,49 @@ +# -*- 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'] diff --git a/tests/unit/test_kotlin_best_practices.py b/tests/unit/test_kotlin_best_practices.py new file mode 100644 index 0000000..4ca5090 --- /dev/null +++ b/tests/unit/test_kotlin_best_practices.py @@ -0,0 +1,48 @@ +# -*- 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'] diff --git a/tests/unit/test_matcher.py b/tests/unit/test_matcher.py index 8202ec3..fca9b89 100644 --- a/tests/unit/test_matcher.py +++ b/tests/unit/test_matcher.py @@ -20,6 +20,17 @@ def test_new_android_kotlin_rules(): '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']) 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 From fcd575280d73af033be2cc513b1f78d409fbeff9 Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 21:20:02 -0700 Subject: [PATCH 15/19] Align packaging and tests with the 1.0.0 stable release. Mark setuptools development status as Production/Stable and drive formatter tests from __version__ instead of hardcoded strings. Co-authored-by: Cursor --- setup.py | 2 +- tests/unit/test_gitlab_sast.py | 7 ++++--- tests/unit/test_mobsfscan.py | 7 ++++--- tests/unit/test_sarif.py | 8 ++++++-- tests/unit/test_sonarqube.py | 5 +++-- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index a12d5a3..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+)'), diff --git a/tests/unit/test_gitlab_sast.py b/tests/unit/test_gitlab_sast.py index 95de021..0f47c45 100644 --- a/tests/unit/test_gitlab_sast.py +++ b/tests/unit/test_gitlab_sast.py @@ -2,6 +2,7 @@ """Tests for GitLab SAST report formatter.""" import json +from mobsfscan import __version__ from mobsfscan.formatters.gitlab_sast import ( SCHEMA_VERSION, gitlab_sast_output, @@ -57,13 +58,13 @@ def test_gitlab_sast_report_shape(tmp_path): }, } outfile = tmp_path / 'gl-sast-report.json' - gitlab_sast_output(str(outfile), scan_results, '0.4.6') + 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'] == '0.4.6' + assert report['scan']['scanner']['version'] == __version__ assert len(report['vulnerabilities']) == 2 by_file = {v['location']['file']: v for v in report['vulnerabilities']} @@ -97,7 +98,7 @@ def test_gitlab_sast_missing_control_location(tmp_path): }, } outfile = tmp_path / 'gl-sast-report.json' - gitlab_sast_output(str(outfile), scan_results, '0.4.6') + gitlab_sast_output(str(outfile), scan_results, __version__) report = json.loads(outfile.read_text()) vuln = report['vulnerabilities'][0] assert vuln['location']['file'] == '.' diff --git a/tests/unit/test_mobsfscan.py b/tests/unit/test_mobsfscan.py index cb0d9c5..7f8fd51 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, @@ -36,15 +37,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_sarif.py b/tests/unit/test_sarif.py index cadb665..cd79a48 100644 --- a/tests/unit/test_sarif.py +++ b/tests/unit/test_sarif.py @@ -2,6 +2,7 @@ """Tests for SARIF rule naming and dashboard metadata.""" import json +from mobsfscan import __version__ from mobsfscan.formatters.sarif import ( build_tags, format_rule_name, @@ -64,10 +65,13 @@ def test_sarif_includes_dashboard_fields(tmp_path): }, } outfile = tmp_path / 'out.sarif' - sarif_output(str(outfile), scan_results, '0.4.6', ['app']) + sarif_output(str(outfile), scan_results, __version__, ['app']) out = json.loads(outfile.read_text()) - rule = out['runs'][0]['tool']['driver']['rules'][0] + 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'] == ( diff --git a/tests/unit/test_sonarqube.py b/tests/unit/test_sonarqube.py index f091e7a..f28f35c 100644 --- a/tests/unit/test_sonarqube.py +++ b/tests/unit/test_sonarqube.py @@ -2,6 +2,7 @@ """Tests for SonarQube generic issue formatter (10.3+).""" import json +from mobsfscan import __version__ from mobsfscan.formatters.sonarqube import ( IMPACT_SEVERITY_MAP, SEVERITY_MAP, @@ -53,7 +54,7 @@ def test_sonarqube_new_format_shape(): }, }, } - raw = sonarqube_output(None, scan_results, '0.4.6') + raw = sonarqube_output(None, scan_results, __version__) report = json.loads(raw) assert set(report.keys()) == {'rules', 'issues'} @@ -100,5 +101,5 @@ def test_sonarqube_new_format_shape(): def test_sonarqube_empty_results(): - report = json.loads(sonarqube_output(None, {'results': {}}, '0.0.0')) + report = json.loads(sonarqube_output(None, {'results': {}}, __version__)) assert report == {'rules': [], 'issues': []} From 3bd2630fafb300bf15964ff15ea52b6ab95331f7 Mon Sep 17 00:00:00 2001 From: Ajin Date: Sun, 9 Aug 2026 21:24:33 -0700 Subject: [PATCH 16/19] Refresh Pipfile.lock and requirements.txt after pipenv sync. Keep the GitHub Action dependency freeze aligned with the locked libsast 3.1.8 hashes and current package name normalization. Co-authored-by: Cursor --- Pipfile.lock | 8 ++++---- requirements.txt | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index b5cfd42..3f7cee8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "579ae981222a3cb09de2a1607a025b6e4f72ae1e123d191ba100ac8ec12af599" + "sha256": "d63d1c7f14a3d770e2e657177df05da593dc770dec48e2542b85370a7d31f3df" }, "pipfile-spec": 6, "requires": { @@ -467,11 +467,11 @@ }, "libsast": { "hashes": [ - "sha256:31a26d76079ac2f4635b6b17784867955c01350255e781f9df0c1d587d408d10", - "sha256:8eba9d45697bca51dffee5c8f96118a4ee1646bd87f06416baf07453602a4eeb" + "sha256:67b2d968b6cc23c6a0694856f9bae5d2cd8ca21bbf45d9f9346fffbfba1f35c0", + "sha256:94dadd4a8aa2447c477b2de4f3b58c39d16412a10f2416bde998517d9656e934" ], "index": "pypi", - "markers": "python_version >= '3.10'", + "markers": "python_version >= '3.10' and python_version < '4.0'", "version": "==3.1.8" }, "markdown-it-py": { diff --git a/requirements.txt b/requirements.txt index 19e7f95..98561fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ 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 @@ -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 From d57c23c3db46897e779fd588600214b4f368326b Mon Sep 17 00:00:00 2001 From: Ajin Abraham Date: Sun, 9 Aug 2026 21:30:12 -0700 Subject: [PATCH 17/19] Prefer cwd-relative paths for XML and Info.plist findings. Stop forcing absolute paths in Android XML and iOS Info.plist results so they match Semgrep/source reporting and reduce ASOC duplicates (#109). Co-authored-by: Cursor --- mobsfscan/ios_plist.py | 3 ++- mobsfscan/manifest.py | 3 ++- mobsfscan/utils.py | 13 +++++++++++++ tests/unit/test_ios_plist.py | 5 +++++ tests/unit/test_report_path.py | 21 +++++++++++++++++++++ tests/unit/test_xml.py | 2 ++ 6 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_report_path.py diff --git a/mobsfscan/ios_plist.py b/mobsfscan/ios_plist.py index 04dcb7a..1314efa 100644 --- a/mobsfscan/ios_plist.py +++ b/mobsfscan/ios_plist.py @@ -4,6 +4,7 @@ from mobsfscan.logger import init_logger from mobsfscan.manifest import add_finding, mobsfscan_format +from mobsfscan.utils import report_path logger = init_logger(__name__) @@ -35,7 +36,7 @@ def scan_plists(plist_paths, validate_func): continue findings.extend( check_transport_security( - plist_path.resolve().as_posix(), + report_path(plist_path), plist, ), ) diff --git a/mobsfscan/manifest.py b/mobsfscan/manifest.py index e8437b4..cd2db4c 100644 --- a/mobsfscan/manifest.py +++ b/mobsfscan/manifest.py @@ -14,6 +14,7 @@ from mobsfscan.manifest_metadata import metadata from mobsfscan.utils import ( is_number, + report_path, valid_host, ) @@ -80,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) diff --git a/mobsfscan/utils.py b/mobsfscan/utils.py index 7d2f17c..1e28c20 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: diff --git a/tests/unit/test_ios_plist.py b/tests/unit/test_ios_plist.py index 4a94871..71be6d9 100644 --- a/tests/unit/test_ios_plist.py +++ b/tests/unit/test_ios_plist.py @@ -27,6 +27,11 @@ def test_ats_info_plist_scan(): 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(): 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_xml.py b/tests/unit/test_xml.py index 70f295d..d56743a 100644 --- a/tests/unit/test_xml.py +++ b/tests/unit/test_xml.py @@ -31,3 +31,5 @@ def test_sensitive_layout_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() From 1040e9626a3a98f9b137a45b92ab17207a480bb2 Mon Sep 17 00:00:00 2001 From: Ajin Abraham Date: Sun, 9 Aug 2026 21:31:07 -0700 Subject: [PATCH 18/19] Clarify SARIF CVSS parsing when metadata is non-numeric. Avoid an empty except in security_severity_score so CodeQL py/empty-except is satisfied while keeping the severity fallback. Co-authored-by: Cursor --- mobsfscan/formatters/sarif.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mobsfscan/formatters/sarif.py b/mobsfscan/formatters/sarif.py index e9f2087..c3f7b99 100644 --- a/mobsfscan/formatters/sarif.py +++ b/mobsfscan/formatters/sarif.py @@ -40,10 +40,11 @@ def security_severity_score(metadata=None): if cvss is not None: try: score = float(cvss) - if 0.1 <= score <= 10.0: - return f'{score:.1f}' except (TypeError, ValueError): - pass + # 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', From 8a3d1913e5d3a096107ef96dca5df7c785bb0d19 Mon Sep 17 00:00:00 2001 From: Ajin Abraham Date: Sun, 9 Aug 2026 22:26:46 -0700 Subject: [PATCH 19/19] Fix best-practice inversion for mixed-language Android/iOS scans. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Invert presence hits for both dialects under a platform (Java∪Kotlin, Swift∪ObjC) so Semgrep BP rules cannot leak, while only reporting missing controls for languages actually present in the scan paths. Co-authored-by: Cursor --- mobsfscan/mobsfscan.py | 109 +++++++++++++---------- mobsfscan/utils.py | 66 ++++++++------ tests/unit/test_java_best_practices.py | 35 ++++++++ tests/unit/test_kotlin_best_practices.py | 6 ++ tests/unit/test_mobsfscan.py | 6 ++ 5 files changed, 148 insertions(+), 74 deletions(-) diff --git a/mobsfscan/mobsfscan.py b/mobsfscan/mobsfscan.py index 505ca3d..5a0a255 100644 --- a/mobsfscan/mobsfscan.py +++ b/mobsfscan/mobsfscan.py @@ -52,55 +52,64 @@ def __init__( } self.xmls = [] self.plists = [] - self.best_practices = None + # 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' - # Android code + best-practice presence checks use Semgrep only. - 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'} - elif suffix in {'.swift', '.m'}: - if suffix == '.swift': - self.best_practices = '.swift' - else: - self.best_practices = '.m' - # Objective-C remains libsast regex; Swift uses Semgrep. - 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'} + 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.""" @@ -210,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'] diff --git a/mobsfscan/utils.py b/mobsfscan/utils.py index 1e28c20..1aefa3f 100644 --- a/mobsfscan/utils.py +++ b/mobsfscan/utils.py @@ -142,42 +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': - java_dir = config.BEST_PRACTICES_DIR / 'java' - for yml in java_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 == '.kt': - # Kotlin Semgrep best practices (same inversion as Java). - kt_dir = config.BEST_PRACTICES_DIR / 'kotlin' - for yml in kt_dir.rglob('*.yaml'): - rules = read_yaml(yml) - 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) - 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) - for rule in rules: - all_rules[rule['id']] = rule - ids.add(rule['id']) return ids, all_rules diff --git a/tests/unit/test_java_best_practices.py b/tests/unit/test_java_best_practices.py index 8fd8586..6b0ee37 100644 --- a/tests/unit/test_java_best_practices.py +++ b/tests/unit/test_java_best_practices.py @@ -47,3 +47,38 @@ def test_java_present_controls_are_inverted_away(): 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 index 4ca5090..655f51a 100644 --- a/tests/unit/test_kotlin_best_practices.py +++ b/tests/unit/test_kotlin_best_practices.py @@ -46,3 +46,9 @@ def test_kotlin_present_controls_are_inverted_away(): 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_mobsfscan.py b/tests/unit/test_mobsfscan.py index 7f8fd51..675f345 100644 --- a/tests/unit/test_mobsfscan.py +++ b/tests/unit/test_mobsfscan.py @@ -13,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',