Detect known secret formats in Android/iOS string analysis (#2640)

Add a shared high-confidence secret detector (secret_detection.py) for known credential formats.
Integrate it into Android/iOS string, resource, binary, and plist secret extraction.
Keep labeled known secrets at the top of Hardcoded Secrets reports.
Remove Amazon LWA rules from android_rules.yaml so SAST stays focused on code-level issues.


Co-authored-by: Ajin Abraham <[email protected]>
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Justin Stewart
2026-08-09 18:12:40 -07:00
committed by GitHub
co-authored by Ajin Abraham Cursor
parent a20d60bf12
commit e41d49bd0d
7 changed files with 253 additions and 15 deletions
@@ -13,6 +13,9 @@ from mobsf.MobSF.utils import (
from mobsf.MobSF.views.home import update_scan_timestamp
from mobsf.StaticAnalyzer.models import StaticAnalyzerAndroid
from mobsf.StaticAnalyzer.models import RecentScansDB
from mobsf.StaticAnalyzer.views.common.secret_detection import (
sort_secrets,
)
from mobsf.StaticAnalyzer.views.common.suppression import (
process_suppression,
process_suppression_manifest,
@@ -86,7 +89,8 @@ def get_context_from_db_entry(db_entry: QuerySet) -> dict:
'behaviour': python_dict(db_entry[0].QUARK),
'trackers': python_dict(db_entry[0].TRACKERS),
'playstore_details': python_dict(db_entry[0].PLAYSTORE_DETAILS),
'secrets': python_list(db_entry[0].SECRETS),
'secrets': sort_secrets(
python_list(db_entry[0].SECRETS)),
'logs': get_scan_logs(db_entry[0].MD5),
'sbom': python_dict(db_entry[0].SBOM),
}
@@ -161,7 +165,7 @@ def get_context_from_analysis(app_dic,
'behaviour': code_an_dic['behaviour'],
'trackers': trackers,
'playstore_details': app_dic['playstore'],
'secrets': code_an_dic['secrets'],
'secrets': sort_secrets(code_an_dic['secrets']),
'logs': get_scan_logs(app_dic['md5']),
'sbom': code_an_dic['sbom'],
}
@@ -8,8 +8,9 @@ from mobsf.StaticAnalyzer.views.common.shared_func import (
strings_and_entropies,
url_n_email_extract,
)
from mobsf.StaticAnalyzer.views.common.entropy import (
get_entropies,
from mobsf.StaticAnalyzer.views.common.secret_detection import (
detect_known_secrets,
get_secrets,
)
from mobsf.MobSF.utils import (
GOOGLE_API_KEY_REGEX,
@@ -37,7 +38,7 @@ def strings_from_so(checksum, elf_strings):
so_urls, so_urls_nf, so_emails_nf = url_n_email_extract(
so_str, so)
sos.append({so: {
'secrets': list(get_entropies(so_str)),
'secrets': list(get_secrets(str_list)),
'strings': list(set(str_list)),
'urls_list': so_urls,
'urls_nf': so_urls_nf,
@@ -93,9 +94,12 @@ def strings_from_apk(checksum, app_dic):
# Check for possible secrets
if is_secret_key(key) and ' ' not in value:
results['secrets'].append(formatted_str)
results['secrets'].extend(
detect_known_secrets(value))
elif app_dic.get('apk_strings'):
# No secret key check for APK strings
results['strings'] = app_dic['apk_strings']
results['secrets'].extend(
detect_known_secrets(results['strings']))
else:
msg = 'Failed to extract String data from APK'
logger.warning(msg)
@@ -0,0 +1,222 @@
"""Known secret pattern detection."""
import re
from mobsf.StaticAnalyzer.views.common.entropy import (
get_entropies,
)
KNOWN_SECRET_PATTERNS = (
(
'GitHub token',
re.compile(
r'(?<![A-Za-z0-9_])'
r'gh[pousr]_[A-Za-z0-9]{36,255}'
r'(?![A-Za-z0-9])'),
),
(
'GitHub fine-grained token',
re.compile(
r'(?<![A-Za-z0-9_])'
r'github_pat_[A-Za-z0-9_]{20,255}'
r'(?![A-Za-z0-9_])'),
),
(
'GitLab personal access token',
re.compile(
r'(?<![A-Za-z0-9_-])'
r'glpat-[A-Za-z0-9_-]{20,255}'
r'(?![A-Za-z0-9_-])'),
),
(
'Slack token',
re.compile(
r'(?<![A-Za-z0-9-])'
r'xox[baprs]-[A-Za-z0-9-]{10,255}'
r'(?![A-Za-z0-9-])'),
),
(
'Slack webhook',
re.compile(
r'https://hooks\.slack\.com/services/'
r'T[A-Z0-9]{8,12}/B[A-Z0-9]{8,12}/'
r'[A-Za-z0-9]{20,64}'
r'(?![A-Za-z0-9])'),
),
(
'Stripe live secret key',
re.compile(
r'(?<![A-Za-z0-9_])'
r'(?:sk|rk)_live_[A-Za-z0-9]{16,255}'
r'(?![A-Za-z0-9])'),
),
(
'SendGrid API key',
re.compile(
r'(?<![A-Za-z0-9_.-])'
r'SG\.[A-Za-z0-9_-]{16,64}\.'
r'[A-Za-z0-9_-]{16,128}'
r'(?![A-Za-z0-9_-])'),
),
(
'npm access token',
re.compile(
r'(?<![A-Za-z0-9_])'
r'npm_[A-Za-z0-9]{36,64}'
r'(?![A-Za-z0-9])'),
),
(
'Mapbox secret token',
re.compile(
r'(?<![A-Za-z0-9_.-])'
r'sk\.[A-Za-z0-9_-]{20,256}\.'
r'[A-Za-z0-9_-]{20,256}'
r'(?![A-Za-z0-9_-])'),
),
(
'Anthropic API key',
re.compile(
r'(?<![A-Za-z0-9_-])'
r'sk-ant-(?:api03|admin01)-'
r'[A-Za-z0-9_-]{93}AA'
r'(?![A-Za-z0-9_-])'),
),
(
'OpenAI API key',
re.compile(
r'(?<![A-Za-z0-9_-])'
r'(?:'
r'sk-(?:proj|svcacct|admin)-'
r'(?:[A-Za-z0-9_-]{58}|[A-Za-z0-9_-]{74})'
r'T3BlbkFJ'
r'(?:[A-Za-z0-9_-]{58}|[A-Za-z0-9_-]{74})'
r'|sk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}'
r')'
r'(?![A-Za-z0-9_-])'),
),
(
'Hugging Face access token',
re.compile(
r'(?<![A-Za-z0-9_])'
r'hf_[A-Za-z]{34}'
r'(?![A-Za-z0-9])'),
),
(
'DigitalOcean token',
re.compile(
r'(?<![A-Za-z0-9_])'
r'(?:doo|dop|dor)_v1_[a-f0-9]{64}'
r'(?![A-Za-z0-9])'),
),
(
'Doppler API token',
re.compile(
r'(?<![A-Za-z0-9_.])'
r'dp\.pt\.[A-Za-z0-9]{43}'
r'(?![A-Za-z0-9])'),
),
(
'Brevo API token',
re.compile(
r'(?<![A-Za-z0-9_-])'
r'xkeysib-[a-f0-9]{64}-[A-Za-z0-9]{16}'
r'(?![A-Za-z0-9])'),
),
(
'Sentry user token',
re.compile(
r'(?<![A-Za-z0-9_])'
r'sntryu_[a-f0-9]{64}'
r'(?![A-Za-z0-9])'),
),
(
'Shopify access token',
re.compile(
r'(?<![A-Za-z0-9_])'
r'(?:shpat|shpca|shppa|shpss)_[A-Fa-f0-9]{32}'
r'(?![A-Fa-f0-9])'),
),
(
'Square access token',
re.compile(
r'(?<![A-Za-z0-9_-])'
r'sq0atp-[A-Za-z0-9_-]{22,60}'
r'(?![A-Za-z0-9_-])'),
),
(
'Private key',
re.compile(
r'-----BEGIN '
r'(?:RSA |EC |DSA |OPENSSH )?'
r'PRIVATE KEY-----'),
),
(
'Amazon LWA client secret',
re.compile(
r'(?<![A-Za-z0-9_.-])'
r'amzn1\.oa2-cs\.v[0-9]+\.[0-9A-Fa-f]{32,128}'
r'(?![0-9A-Fa-f])'),
),
(
'Amazon LWA access token',
re.compile(
r'(?<![A-Za-z0-9_-])'
r'Atza\|[A-Za-z0-9_-]{20,2043}'
r'(?![A-Za-z0-9_-])'),
),
(
'Amazon LWA refresh token',
re.compile(
r'(?<![A-Za-z0-9_-])'
r'Atzr\|[A-Za-z0-9_-]{20,2043}'
r'(?![A-Za-z0-9_-])'),
),
)
KNOWN_SECRET_PREFIXES = tuple(
f'{name}: ' for name, _pattern in KNOWN_SECRET_PATTERNS)
def _strings(data):
"""Normalize a string or iterable of strings to a tuple."""
if data is None:
return ()
if isinstance(data, str):
return (data,)
return tuple(data)
def detect_known_secrets(data):
"""Detect and label secrets with known value formats."""
secrets = set()
for value in _strings(data):
if not isinstance(value, str):
continue
for name, pattern in KNOWN_SECRET_PATTERNS:
for match in pattern.finditer(value):
secrets.add(f'{name}: {match.group()}')
return secrets
def get_secrets(data):
"""Detect known-format and high-entropy secrets."""
secrets = set()
for value in _strings(data):
if not isinstance(value, str):
continue
scrubbed = value
for name, pattern in KNOWN_SECRET_PATTERNS:
for match in pattern.finditer(value):
secrets.add(f'{name}: {match.group()}')
scrubbed = pattern.sub('', scrubbed)
secrets.update(get_entropies((scrubbed,)))
return secrets
def sort_secrets(secrets):
"""Place labeled known-format secrets before heuristic findings."""
return sorted(
set(secrets),
key=lambda secret: (
not secret.startswith(KNOWN_SECRET_PREFIXES),
secret.casefold(),
),
)
@@ -43,8 +43,8 @@ from mobsf.MobSF.views.scanning import (
from mobsf.StaticAnalyzer.views.comparer import (
generic_compare,
)
from mobsf.StaticAnalyzer.views.common.entropy import (
get_entropies,
from mobsf.StaticAnalyzer.views.common.secret_detection import (
get_secrets,
)
from mobsf.MobSF.views.authentication import (
login_required,
@@ -494,7 +494,7 @@ def strings_and_entropies(checksum, src, exts):
continue
data['strings'].add(string)
if data['strings']:
data['secrets'] = get_entropies(data['strings'])
data['secrets'] = get_secrets(data['strings'])
except Exception as exp:
msg = 'Failed to extract String values and entropies from Code'
logger.exception(msg)
@@ -12,6 +12,9 @@ from mobsf.MobSF.utils import (
from mobsf.MobSF.views.home import update_scan_timestamp
from mobsf.StaticAnalyzer.models import StaticAnalyzerIOS
from mobsf.StaticAnalyzer.models import RecentScansDB
from mobsf.StaticAnalyzer.views.common.secret_detection import (
sort_secrets,
)
from mobsf.StaticAnalyzer.views.common.suppression import (
process_suppression,
)
@@ -70,7 +73,8 @@ def get_context_from_db_entry(db_entry):
'strings': python_list(db_entry[0].STRINGS),
'firebase_urls': python_list(db_entry[0].FIREBASE_URLS),
'appstore_details': python_dict(db_entry[0].APPSTORE_DETAILS),
'secrets': python_list(db_entry[0].SECRETS),
'secrets': sort_secrets(
python_list(db_entry[0].SECRETS)),
'trackers': python_dict(db_entry[0].TRACKERS),
'logs': get_scan_logs(db_entry[0].MD5),
}
@@ -134,7 +138,7 @@ def get_context_from_analysis(app_dict,
'strings': bin_dict['strings'],
'firebase_urls': code_dict['firebase'],
'appstore_details': app_dict['appstore'],
'secrets': app_dict['secrets'],
'secrets': sort_secrets(app_dict['secrets']),
'trackers': code_dict['trackers'],
'logs': get_scan_logs(app_dict['md5_hash']),
}
@@ -25,6 +25,9 @@ from mobsf.StaticAnalyzer.views.ios.kb.permission_analysis import (
from mobsf.StaticAnalyzer.views.ios.app_transport_security import (
check_transport_security,
)
from mobsf.StaticAnalyzer.views.common.secret_detection import (
detect_known_secrets,
)
from mobsf.StaticAnalyzer.views.common.shared_func import (
is_secret_key,
)
@@ -247,6 +250,7 @@ def get_plist_secrets(checksum, app_dir):
for i in Path(app_dir).rglob('*.plist'):
xml_string = i.read_text('utf-8', 'ignore')
result_list.update(detect_known_secrets(xml_string))
xml_list = xml_string.split('\n')
for index, line in enumerate(xml_list):
if '<key>' in line and is_secret_key(_remove_tags(line)):
+4 -4
View File
@@ -3,8 +3,8 @@
import io
import logging
from mobsf.StaticAnalyzer.views.common.entropy import (
get_entropies,
from mobsf.StaticAnalyzer.views.common.secret_detection import (
get_secrets,
)
from mobsf.StaticAnalyzer.views.common.shared_func import (
url_n_email_extract,
@@ -73,7 +73,7 @@ def get_strings_metadata(app_dict, bin_dict, all_files, dy_list):
urls_list = []
urls_n_files = []
emails_n_files = []
secrets = []
secrets = list(get_secrets(bin_dict['strings']))
# IPA URL and Email Extract
str_meta = extract_urls_n_email(
@@ -97,7 +97,7 @@ def get_strings_metadata(app_dict, bin_dict, all_files, dy_list):
urls_list.extend(urls)
urls_n_files.extend(urls_nf)
emails_n_files.extend(emails_nf)
secrets.extend(get_entropies(dy_str))
secrets.extend(get_secrets(s))
bin_dict['strings'].extend(dy_strings)
bin_dict['strings'] = list(