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