diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index f6d20f254..a7b4242f1 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -47,6 +47,8 @@ pipenv run coverage xml -o converter/coverage/cobertura.xml - name: Check test coverage - Report run: pipenv run coverage report --fail-under 85 scripts/convert* + - name: Check Mobile App enrichment coverage - Report + run: pipenv run coverage report --fail-under 95 scripts/enrich_mobileapp_mappings/*.py - name: Check PDF generation test coverage - Report run: pipenv run coverage report --fail-under 52 scripts/pdf_generation/*.py # Upload Code Coverage for Codeclimate diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 6c936b2c6..e364d6e54 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -42,6 +42,8 @@ jobs: run: pipenv run coverage xml - name: Check test coverage - Report run: pipenv run coverage report --fail-under 95 scripts/convert* + - name: Check Mobile App enrichment coverage - Report + run: pipenv run coverage report --fail-under 95 scripts/enrich_mobileapp_mappings/*.py - name: Check PDF generation test coverage - Report run: pipenv run coverage report --fail-under 52 scripts/pdf_generation/*.py # Check formatting of files diff --git a/scripts/README.md b/scripts/README.md index 6d8cbaa18..38bd1df15 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -273,40 +273,20 @@ pipenv run python scripts/convert_mastg_map.py -i ../mastg --maswe-input-path .. ### Enriching Mobile App Mappings -The `scripts/enrich_mobileapp_mappings.py` script adds MASTG, MASWE, threat, and attack-vector metadata to Mobile App Edition card mappings. +The [`scripts/enrich_mobileapp_mappings/`](enrich_mobileapp_mappings/) module adds MASTG, MASWE, MASVS, threat, and attack-vector metadata to Mobile App Edition card mappings. Its package README documents the module layout, enrichment rules, and validation commands. ```bash -python scripts/enrich_mobileapp_mappings.py --help -usage: enrich_mobileapp_mappings.py [-h] [-e EDITION] [-v VERSION] [-s SOURCE_DIR] [-i INPUT_PATH] - [--mastg-path MASTG_PATH] [--maswe-path MASWE_PATH] [-o OUTPUT_PATH] - -Enrich Mobile card mappings with MASTG and MASWE metadata - -options: - -h, --help show this help message and exit - -e EDITION, --edition EDITION - Cornucopia edition, for example mobileapp - -v VERSION, --version VERSION - Cornucopia version, for example 2.0 - -s SOURCE_DIR, --source-dir SOURCE_DIR - -i INPUT_PATH, --input-path INPUT_PATH - Card mapping YAML to enrich - --mastg-path MASTG_PATH - Generated MASTG metadata YAML - --maswe-path MASWE_PATH - Generated MASWE metadata YAML - -o OUTPUT_PATH, --output-path OUTPUT_PATH - Enriched mapping YAML; defaults to input +python -m scripts.enrich_mobileapp_mappings --help ``` **Example usage:** ```bash # Enrich the default Mobile App Edition mapping file in place -pipenv run python scripts/enrich_mobileapp_mappings.py +pipenv run python -m scripts.enrich_mobileapp_mappings # Enrich an explicit mapping file and write to a new path -pipenv run python scripts/enrich_mobileapp_mappings.py -i source/mobileapp-mappings-2.0.yaml \ +pipenv run python -m scripts.enrich_mobileapp_mappings -i source/mobileapp-mappings-2.0.yaml \ --mastg-path source/mobileapp-mastg-2.0.yaml --maswe-path source/mobileapp-maswe-2.0.yaml \ -o source/mobileapp-mappings-2.0-enriched.yaml ``` @@ -318,6 +298,11 @@ pipenv run python scripts/enrich_mobileapp_mappings.py -i source/mobileapp-mappi - MASWE metadata: `source/mobileapp-maswe-2.0.yaml` - Output: overwrites the input mapping file +For each card, the module matches `owasp_maswe` values against MASWE codes in +`mobileapp-maswe-2.0.yaml` and writes the source-ordered, deduplicated union of +their `owasp_masvs` values. Missing legacy MASWE codes are reported as +warnings and do not create invented mappings. + ## Contributing to Development ### LibreOffice Installation diff --git a/scripts/enrich_mobileapp_mappings.py b/scripts/enrich_mobileapp_mappings.py deleted file mode 100644 index 76cc91c7b..000000000 --- a/scripts/enrich_mobileapp_mappings.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env python3 -"""Enrich Cornucopia Mobile card mappings from generated MASTG and MASWE metadata.""" - -import argparse -import logging -import re -import sys -from pathlib import Path -from typing import Any - -import yaml -from pathvalidate.argparse import validate_filepath_arg - -MAX_YAML_FILE_SIZE_BYTES = 2 * 1024 * 1024 -FILENAME_COMPONENT_PATTERN = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}") - - -class UniqueKeySafeLoader(yaml.SafeLoader): - """Safe YAML loader that rejects ambiguous duplicate mapping keys.""" - - def construct_mapping(self, node: yaml.MappingNode, deep: bool = False) -> dict[Any, Any]: - mapping: dict[Any, Any] = {} - for key_node, value_node in node.value: - key = self.construct_object(key_node, deep=deep) - if key in mapping: - raise ValueError(f"Duplicate YAML key: {key!r}") - mapping[key] = self.construct_object(value_node, deep=deep) - return mapping - - -class LeadingZeroStringDumper(yaml.SafeDumper): - """YAML dumper that preserves zero-padded identifiers as strings.""" - - -def represent_string(dumper: LeadingZeroStringDumper, value: str) -> yaml.ScalarNode: - """Use single quotes for digit-only strings that start with zero.""" - style = "'" if re.fullmatch(r"0\d+", value) else None - return dumper.represent_scalar("tag:yaml.org,2002:str", value, style=style) - - -def represent_list(dumper: LeadingZeroStringDumper, value: list[Any]) -> yaml.SequenceNode: - """Keep scalar lists inline while preserving block layout for cards and suits.""" - return dumper.represent_sequence( - "tag:yaml.org,2002:seq", value, flow_style=all(not isinstance(item, (dict, list)) for item in value) - ) - - -LeadingZeroStringDumper.add_representer(str, represent_string) -LeadingZeroStringDumper.add_representer(list, represent_list) - - -DEFAULT_SOURCE_DIR = Path(__file__).parent / "../source" -MAPPING_FIELDS = ("owasp_mastg", "owasp_mastg_know", "owasp_mastg_best", "owasp_maswe") - - -def parse_arguments(input_args: list[str]) -> argparse.Namespace: - """Parse source and output locations for one edition/version mapping set.""" - parser = argparse.ArgumentParser(description="Enrich Mobile card mappings with MASTG and MASWE metadata") - parser.add_argument( - "-e", - "--edition", - type=validate_filename_component, - default="mobileapp", - help="Cornucopia edition, for example mobileapp", - ) - parser.add_argument( - "-v", "--version", type=validate_filename_component, default="2.0", help="Cornucopia version, for example 2.0" - ) - parser.add_argument("-s", "--source-dir", type=validate_filepath_arg, default=DEFAULT_SOURCE_DIR) - parser.add_argument("-i", "--input-path", type=validate_filepath_arg, help="Card mapping YAML to enrich") - parser.add_argument("--mastg-path", type=validate_filepath_arg, help="Generated MASTG metadata YAML") - parser.add_argument("--maswe-path", type=validate_filepath_arg, help="Generated MASWE metadata YAML") - parser.add_argument( - "-o", "--output-path", type=validate_filepath_arg, help="Enriched mapping YAML; defaults to input" - ) - return parser.parse_args(input_args) - - -def validate_filename_component(value: str) -> str: - """Allow only a single filename component for generated mapping names.""" - if not FILENAME_COMPONENT_PATTERN.fullmatch(value) or value in {".", ".."}: - raise argparse.ArgumentTypeError("must contain only letters, digits, dots, underscores, and hyphens") - return value - - -def load_yaml_file(path: Path) -> dict[str, Any]: - """Load a YAML mapping or raise a clear error for invalid input.""" - if path.stat().st_size > MAX_YAML_FILE_SIZE_BYTES: - raise ValueError(f"{path}: file exceeds {MAX_YAML_FILE_SIZE_BYTES} byte limit") - data = yaml.load(path.read_text(encoding="utf-8"), Loader=UniqueKeySafeLoader) - if not isinstance(data, dict): - raise ValueError(f"{path}: expected a mapping") - return data - - -def merge_unique(existing: list[str], additions: list[str]) -> list[str]: - """Append source-ordered additions without duplicating identifiers.""" - result = list(existing) - for value in additions: - if value not in result: - result.append(value) - return result - - -def string_list(value: Any, context: str) -> list[str]: - """Validate an optional list of identifier strings.""" - if value is None: - return [] - if not isinstance(value, list) or not all(isinstance(item, str) for item in value): - raise ValueError(f"{context}: expected a list of strings") - return value - - -def infer_mastg_mappings(card_id: str, test_ids: list[str], mastg_data: dict[str, Any]) -> dict[str, list[str]]: - """Collect mappings inferred from available MASTG test metadata.""" - inferred: dict[str, list[str]] = {field: [] for field in MAPPING_FIELDS} - for test_id in test_ids: - if test_id == "-": - continue - test_mapping = mastg_data.get(test_id) - if not isinstance(test_mapping, dict): - logging.warning("%s: skipping MASTG test %r because it is absent from generated metadata", card_id, test_id) - continue - inferred["owasp_mastg"].append(test_id) - for field in ("owasp_mastg_know", "owasp_mastg_best", "owasp_maswe"): - inferred[field] = merge_unique( - inferred[field], string_list(test_mapping.get(field), f"MASTG {test_id} {field}") - ) - return inferred - - -def collect_maswe_references( - card_id: str, weakness_ids: list[str], maswe_data: dict[str, Any] -) -> tuple[dict[str, str], dict[str, str]]: - """Collect threat and attack descriptions for inferred MASWE weaknesses.""" - threats: dict[str, str] = {} - attack_vectors: dict[str, str] = {} - for weakness_id in weakness_ids: - weakness_mapping = maswe_data.get(weakness_id) - if not isinstance(weakness_mapping, dict): - raise ValueError(f"{card_id}: MASWE {weakness_id!r} is missing from generated metadata") - for field, destination in (("owasp_mas_threat", threats), ("owasp_mas_attack", attack_vectors)): - references = weakness_mapping.get(field, {}) - if not isinstance(references, dict) or not all( - isinstance(identifier, str) and isinstance(description, str) - for identifier, description in references.items() - ): - raise ValueError(f"MASWE {weakness_id} {field}: expected identifier-to-description mapping") - destination.update(references) - return threats, attack_vectors - - -def enrich_card(card: dict[str, Any], mastg_data: dict[str, Any], maswe_data: dict[str, Any]) -> None: - """Merge MASTG siblings and their MASWE threat and attack descriptions into one card.""" - card_id = card.get("id", "unknown card") - test_ids = string_list(card.get("owasp_mastg"), f"{card_id} owasp_mastg") - inferred = infer_mastg_mappings(card_id, test_ids, mastg_data) - threats, attack_vectors = collect_maswe_references(card_id, inferred["owasp_maswe"], maswe_data) - - for field in MAPPING_FIELDS: - card[field] = merge_unique(string_list(card.get(field), f"{card_id} {field}"), inferred[field]) - if threats: - card["threat"] = threats - if attack_vectors: - card["attack_vector"] = attack_vectors - - -def enrich_mappings( - mapping_data: dict[str, Any], mastg_data: dict[str, Any], maswe_data: dict[str, Any] -) -> dict[str, Any]: - """Enrich every card mapping in a Mobile edition mapping document.""" - suits = mapping_data.get("suits") - if not isinstance(suits, list): - raise ValueError("Card mappings: expected suits list") - for suit in suits: - if not isinstance(suit, dict) or not isinstance(suit.get("cards"), list): - raise ValueError("Card mappings: each suit must contain a cards list") - for card in suit["cards"]: - if not isinstance(card, dict): - raise ValueError("Card mappings: each card must be a mapping") - enrich_card(card, mastg_data, maswe_data) - return mapping_data - - -def save_yaml_file(path: Path, data: dict[str, Any]) -> None: - """Write enriched YAML with stable key order and safe zero-padded identifiers.""" - path.parent.mkdir(parents=True, exist_ok=True) - with path.open("w", encoding="utf-8", newline="\n") as output_file: - yaml.dump(data, output_file, Dumper=LeadingZeroStringDumper, allow_unicode=True, sort_keys=False) - - -def main() -> None: - """Load generated metadata, enrich card mappings, and write the target YAML file.""" - args = parse_arguments(sys.argv[1:]) - source_dir = Path(args.source_dir).resolve() - mapping_path = ( - Path(args.input_path).resolve() - if args.input_path - else source_dir / f"{args.edition}-mappings-{args.version}.yaml" - ) - mastg_path = ( - Path(args.mastg_path).resolve() if args.mastg_path else source_dir / f"{args.edition}-mastg-{args.version}.yaml" - ) - maswe_path = ( - Path(args.maswe_path).resolve() if args.maswe_path else source_dir / f"{args.edition}-maswe-{args.version}.yaml" - ) - output_path = Path(args.output_path).resolve() if args.output_path else mapping_path - save_yaml_file( - output_path, - enrich_mappings(load_yaml_file(mapping_path), load_yaml_file(mastg_path), load_yaml_file(maswe_path)), - ) - - -if __name__ == "__main__": - main() diff --git a/scripts/enrich_mobileapp_mappings/README.md b/scripts/enrich_mobileapp_mappings/README.md new file mode 100644 index 000000000..f43c37082 --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/README.md @@ -0,0 +1,81 @@ +# Mobile App Mapping Enrichment + +This module enriches Mobile App Edition card mappings with metadata generated +from the OWASP MASTG and MASWE data: + +- MASTG test, knowledge, and best-practice mappings +- MASWE weakness mappings +- MASVS values associated with each referenced MASWE weakness +- MAS threat and attack-vector descriptions + +## Usage + +Run the module from the repository root: + +```bash +python -m scripts.enrich_mobileapp_mappings --help +``` + +Enrich the default Mobile App Edition mapping in place: + +```bash +python -m scripts.enrich_mobileapp_mappings +``` + +Use explicit input and output paths: + +```bash +python -m scripts.enrich_mobileapp_mappings \ + --input-path source/mobileapp-mappings-2.0.yaml \ + --mastg-path source/mobileapp-mastg-2.0.yaml \ + --maswe-path source/mobileapp-maswe-2.0.yaml \ + --output-path source/mobileapp-mappings-2.0-enriched.yaml +``` + +The default paths are: + +| File | Default | +|---|---| +| Card mappings | `source/mobileapp-mappings-2.0.yaml` | +| MASTG metadata | `source/mobileapp-mastg-2.0.yaml` | +| MASWE metadata | `source/mobileapp-maswe-2.0.yaml` | +| Output | The card mappings input file | + +For each card, `owasp_maswe` values are matched against the root MASWE codes +in the MASWE metadata file. The card's `owasp_masvs` list is the +source-ordered, deduplicated union of the matching MASWE `owasp_masvs` lists. +It is recomputed on every run, so obsolete values from a previous generated +mapping are removed. +Missing legacy MASWE codes are reported as warnings and do not create +invented mappings. + +## Module layout + +| File | Responsibility | +|---|---| +| `__init__.py` | Public API and command orchestration | +| `__main__.py` | `python -m scripts.enrich_mobileapp_mappings` entry point | +| `arguments.py` | CLI argument parsing and filename validation | +| `card.py` | Enrichment of one card | +| `document.py` | Validation and enrichment of the card document | +| `mastg.py` | MASTG-derived mappings | +| `maswe.py` | MASWE threat and attack references | +| `masvs.py` | MASVS mappings derived from MASWE | +| `yaml_loader.py` | Safe YAML loading and duplicate-key detection | +| `yaml_output.py` | Stable YAML serialization | +| `utils.py` | Shared list validation and deduplication | + +## Validation + +From the repository root: + +```bash +python -m unittest discover --start-directory tests/scripts \ + --pattern enrich_mobileapp_mappings_utest.py +python -m black --line-length=120 --check . +python -m flake8 --max-line-length=120 --max-complexity=10 \ + --ignore=E203,W503 scripts/enrich_mobileapp_mappings +python -m mypy --namespace-packages --strict scripts/enrich_mobileapp_mappings +python -m coverage run --branch -m unittest tests/scripts/enrich_mobileapp_mappings_utest.py +python -m coverage report --fail-under 95 scripts/enrich_mobileapp_mappings/*.py +``` diff --git a/scripts/enrich_mobileapp_mappings/__init__.py b/scripts/enrich_mobileapp_mappings/__init__.py new file mode 100644 index 000000000..0c7759210 --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/__init__.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Enrich Cornucopia Mobile card mappings from generated MASTG and MASWE metadata.""" + +import sys +from pathlib import Path +from typing import Any + +from .arguments import ( + DEFAULT_SOURCE_DIR, + parse_arguments, + validate_filename_component, +) +from .card import enrich_card +from .constants import MAPPING_FIELDS +from .document import enrich_mappings +from .masvs import infer_masvs_mappings +from .mastg import infer_mastg_mappings +from .maswe import collect_maswe_references +from .utils import merge_unique, string_list +from .yaml_loader import ( + UniqueKeySafeLoader, + load_yaml_file as _load_yaml_file, +) +from .yaml_output import ( + LeadingZeroStringDumper, + represent_list, + represent_string, + save_yaml_file, +) + +MAX_YAML_FILE_SIZE_BYTES = 2 * 1024 * 1024 + +__all__ = [ + "DEFAULT_SOURCE_DIR", + "LeadingZeroStringDumper", + "MAPPING_FIELDS", + "MAX_YAML_FILE_SIZE_BYTES", + "UniqueKeySafeLoader", + "collect_maswe_references", + "enrich_card", + "enrich_mappings", + "infer_masvs_mappings", + "infer_mastg_mappings", + "load_yaml_file", + "main", + "merge_unique", + "parse_arguments", + "represent_list", + "represent_string", + "save_yaml_file", + "string_list", + "validate_filename_component", +] + + +def load_yaml_file(path: Path) -> dict[str, Any]: + """Load a YAML mapping using the configured input size limit.""" + return _load_yaml_file(path, MAX_YAML_FILE_SIZE_BYTES) + + +def main() -> None: + """Load generated metadata, enrich card mappings, and write the target YAML file.""" + args = parse_arguments(sys.argv[1:]) + source_dir = Path(args.source_dir).resolve() + mapping_path = ( + Path(args.input_path).resolve() + if args.input_path + else source_dir / f"{args.edition}-mappings-{args.version}.yaml" + ) + mastg_path = ( + Path(args.mastg_path).resolve() if args.mastg_path else source_dir / f"{args.edition}-mastg-{args.version}.yaml" + ) + maswe_path = ( + Path(args.maswe_path).resolve() if args.maswe_path else source_dir / f"{args.edition}-maswe-{args.version}.yaml" + ) + output_path = Path(args.output_path).resolve() if args.output_path else mapping_path + save_yaml_file( + output_path, + enrich_mappings(load_yaml_file(mapping_path), load_yaml_file(mastg_path), load_yaml_file(maswe_path)), + ) diff --git a/scripts/enrich_mobileapp_mappings/__main__.py b/scripts/enrich_mobileapp_mappings/__main__.py new file mode 100644 index 000000000..12a20368a --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/__main__.py @@ -0,0 +1,12 @@ +"""Run the Mobile App mapping enrichment module.""" + +import sys +from pathlib import Path + +if __package__ in {None, ""}: # pragma: no cover + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +from scripts.enrich_mobileapp_mappings import main # pragma: no cover + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/scripts/enrich_mobileapp_mappings/arguments.py b/scripts/enrich_mobileapp_mappings/arguments.py new file mode 100644 index 000000000..179bdaeaf --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/arguments.py @@ -0,0 +1,38 @@ +import argparse +import re +from pathlib import Path + +from pathvalidate.argparse import validate_filepath_arg + +DEFAULT_SOURCE_DIR = Path(__file__).resolve().parents[2] / "source" +FILENAME_COMPONENT_PATTERN = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}") + + +def validate_filename_component(value: str) -> str: + """Allow only a single filename component for generated mapping names.""" + if not FILENAME_COMPONENT_PATTERN.fullmatch(value) or value in {".", ".."}: + raise argparse.ArgumentTypeError("must contain only letters, digits, dots, underscores, and hyphens") + return value + + +def parse_arguments(input_args: list[str]) -> argparse.Namespace: + """Parse source and output locations for one edition/version mapping set.""" + parser = argparse.ArgumentParser(description="Enrich Mobile card mappings with MASTG and MASWE metadata") + parser.add_argument( + "-e", + "--edition", + type=validate_filename_component, + default="mobileapp", + help="Cornucopia edition, for example mobileapp", + ) + parser.add_argument( + "-v", "--version", type=validate_filename_component, default="2.0", help="Cornucopia version, for example 2.0" + ) + parser.add_argument("-s", "--source-dir", type=validate_filepath_arg, default=DEFAULT_SOURCE_DIR) + parser.add_argument("-i", "--input-path", type=validate_filepath_arg, help="Card mapping YAML to enrich") + parser.add_argument("--mastg-path", type=validate_filepath_arg, help="Generated MASTG metadata YAML") + parser.add_argument("--maswe-path", type=validate_filepath_arg, help="Generated MASWE metadata YAML") + parser.add_argument( + "-o", "--output-path", type=validate_filepath_arg, help="Enriched mapping YAML; defaults to input" + ) + return parser.parse_args(input_args) diff --git a/scripts/enrich_mobileapp_mappings/card.py b/scripts/enrich_mobileapp_mappings/card.py new file mode 100644 index 000000000..d3f9df42b --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/card.py @@ -0,0 +1,25 @@ +from typing import Any + +from .constants import MAPPING_FIELDS +from .masvs import infer_masvs_mappings +from .mastg import infer_mastg_mappings +from .maswe import collect_maswe_references +from .utils import merge_unique, string_list + + +def enrich_card(card: dict[str, Any], mastg_data: dict[str, Any], maswe_data: dict[str, Any]) -> None: + """Merge MASTG siblings and their MASWE and MASVS metadata into one card.""" + card_id = card.get("id", "unknown card") + test_ids = string_list(card.get("owasp_mastg"), f"{card_id} owasp_mastg") + inferred = infer_mastg_mappings(card_id, test_ids, mastg_data) + threats, attack_vectors = collect_maswe_references(card_id, inferred["owasp_maswe"], maswe_data) + + for field in MAPPING_FIELDS: + card[field] = merge_unique(string_list(card.get(field), f"{card_id} {field}"), inferred[field]) + card["owasp_masvs"] = infer_masvs_mappings( + card_id, string_list(card["owasp_maswe"], f"{card_id} owasp_maswe"), maswe_data + ) + if threats: + card["threat"] = threats + if attack_vectors: + card["attack_vector"] = attack_vectors diff --git a/scripts/enrich_mobileapp_mappings/constants.py b/scripts/enrich_mobileapp_mappings/constants.py new file mode 100644 index 000000000..50dfb01d8 --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/constants.py @@ -0,0 +1 @@ +MAPPING_FIELDS = ("owasp_mastg", "owasp_mastg_know", "owasp_mastg_best", "owasp_maswe") diff --git a/scripts/enrich_mobileapp_mappings/document.py b/scripts/enrich_mobileapp_mappings/document.py new file mode 100644 index 000000000..275e09f51 --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/document.py @@ -0,0 +1,20 @@ +from typing import Any + +from .card import enrich_card + + +def enrich_mappings( + mapping_data: dict[str, Any], mastg_data: dict[str, Any], maswe_data: dict[str, Any] +) -> dict[str, Any]: + """Enrich every card mapping in a Mobile edition mapping document.""" + suits = mapping_data.get("suits") + if not isinstance(suits, list): + raise ValueError("Card mappings: expected suits list") + for suit in suits: + if not isinstance(suit, dict) or not isinstance(suit.get("cards"), list): + raise ValueError("Card mappings: each suit must contain a cards list") + for card in suit["cards"]: + if not isinstance(card, dict): + raise ValueError("Card mappings: each card must be a mapping") + enrich_card(card, mastg_data, maswe_data) + return mapping_data diff --git a/scripts/enrich_mobileapp_mappings/mastg.py b/scripts/enrich_mobileapp_mappings/mastg.py new file mode 100644 index 000000000..bbbad9b66 --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/mastg.py @@ -0,0 +1,23 @@ +import logging +from typing import Any + +from .constants import MAPPING_FIELDS +from .utils import merge_unique, string_list + + +def infer_mastg_mappings(card_id: str, test_ids: list[str], mastg_data: dict[str, Any]) -> dict[str, list[str]]: + """Collect mappings inferred from available MASTG test metadata.""" + inferred: dict[str, list[str]] = {field: [] for field in MAPPING_FIELDS} + for test_id in test_ids: + if test_id == "-": + continue + test_mapping = mastg_data.get(test_id) + if not isinstance(test_mapping, dict): + logging.warning("%s: skipping MASTG test %r because it is absent from generated metadata", card_id, test_id) + continue + inferred["owasp_mastg"].append(test_id) + for field in ("owasp_mastg_know", "owasp_mastg_best", "owasp_maswe"): + inferred[field] = merge_unique( + inferred[field], string_list(test_mapping.get(field), f"MASTG {test_id} {field}") + ) + return inferred diff --git a/scripts/enrich_mobileapp_mappings/masvs.py b/scripts/enrich_mobileapp_mappings/masvs.py new file mode 100644 index 000000000..425e59118 --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/masvs.py @@ -0,0 +1,21 @@ +import logging +from typing import Any + +from .utils import merge_unique, string_list + + +def infer_masvs_mappings(card_id: str, weakness_ids: list[str], maswe_data: dict[str, Any]) -> list[str]: + """Collect MASVS mappings from MASWE metadata referenced by a card.""" + inferred: list[str] = [] + for weakness_id in weakness_ids: + weakness_mapping = maswe_data.get(weakness_id) + if not isinstance(weakness_mapping, dict): + logging.warning( + "%s: skipping MASWE weakness %r because it is absent from generated metadata", card_id, weakness_id + ) + continue + inferred = merge_unique( + inferred, + string_list(weakness_mapping.get("owasp_masvs"), f"MASWE {weakness_id} owasp_masvs"), + ) + return inferred diff --git a/scripts/enrich_mobileapp_mappings/maswe.py b/scripts/enrich_mobileapp_mappings/maswe.py new file mode 100644 index 000000000..ba2fec8c6 --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/maswe.py @@ -0,0 +1,22 @@ +from typing import Any + + +def collect_maswe_references( + card_id: str, weakness_ids: list[str], maswe_data: dict[str, Any] +) -> tuple[dict[str, str], dict[str, str]]: + """Collect threat and attack descriptions for inferred MASWE weaknesses.""" + threats: dict[str, str] = {} + attack_vectors: dict[str, str] = {} + for weakness_id in weakness_ids: + weakness_mapping = maswe_data.get(weakness_id) + if not isinstance(weakness_mapping, dict): + raise ValueError(f"{card_id}: MASWE {weakness_id!r} is missing from generated metadata") + for field, destination in (("owasp_mas_threat", threats), ("owasp_mas_attack", attack_vectors)): + references = weakness_mapping.get(field, {}) + if not isinstance(references, dict) or not all( + isinstance(identifier, str) and isinstance(description, str) + for identifier, description in references.items() + ): + raise ValueError(f"MASWE {weakness_id} {field}: expected identifier-to-description mapping") + destination.update(references) + return threats, attack_vectors diff --git a/scripts/enrich_mobileapp_mappings/utils.py b/scripts/enrich_mobileapp_mappings/utils.py new file mode 100644 index 000000000..656bfdede --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/utils.py @@ -0,0 +1,19 @@ +from typing import Any + + +def merge_unique(existing: list[str], additions: list[str]) -> list[str]: + """Append source-ordered additions without duplicating identifiers.""" + result = list(existing) + for value in additions: + if value not in result: + result.append(value) + return result + + +def string_list(value: Any, context: str) -> list[str]: + """Validate an optional list of identifier strings.""" + if value is None: + return [] + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError(f"{context}: expected a list of strings") + return value diff --git a/scripts/enrich_mobileapp_mappings/yaml_loader.py b/scripts/enrich_mobileapp_mappings/yaml_loader.py new file mode 100644 index 000000000..21557c512 --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/yaml_loader.py @@ -0,0 +1,28 @@ +from pathlib import Path +from typing import Any + +import yaml + + +class UniqueKeySafeLoader(yaml.SafeLoader): + """Safe YAML loader that rejects ambiguous duplicate mapping keys.""" + + def construct_mapping(self, node: yaml.MappingNode, deep: bool = False) -> dict[Any, Any]: + mapping: dict[Any, Any] = {} + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + if key in mapping: + raise ValueError(f"Duplicate YAML key: {key!r}") + mapping[key] = self.construct_object(value_node, deep=deep) + return mapping + + +def load_yaml_file(path: Path, max_size_bytes: int) -> dict[str, Any]: + """Load a YAML mapping or raise a clear error for invalid input.""" + if path.stat().st_size > max_size_bytes: + raise ValueError(f"{path}: file exceeds {max_size_bytes} byte limit") + # The custom loader inherits SafeLoader and adds duplicate-key rejection. + data = yaml.load(path.read_text(encoding="utf-8"), Loader=UniqueKeySafeLoader) # nosec B506 + if not isinstance(data, dict): + raise ValueError(f"{path}: expected a mapping") + return data diff --git a/scripts/enrich_mobileapp_mappings/yaml_output.py b/scripts/enrich_mobileapp_mappings/yaml_output.py new file mode 100644 index 000000000..c8bf8c08e --- /dev/null +++ b/scripts/enrich_mobileapp_mappings/yaml_output.py @@ -0,0 +1,33 @@ +import re +from pathlib import Path +from typing import Any + +import yaml + + +class LeadingZeroStringDumper(yaml.SafeDumper): + """YAML dumper that preserves zero-padded identifiers as strings.""" + + +def represent_string(dumper: LeadingZeroStringDumper, value: str) -> yaml.ScalarNode: + """Use single quotes for digit-only strings that start with zero.""" + style = "'" if re.fullmatch(r"0\d+", value) else None + return dumper.represent_scalar("tag:yaml.org,2002:str", value, style=style) + + +def represent_list(dumper: LeadingZeroStringDumper, value: list[Any]) -> yaml.SequenceNode: + """Keep scalar lists inline while preserving block layout for cards and suits.""" + return dumper.represent_sequence( + "tag:yaml.org,2002:seq", value, flow_style=all(not isinstance(item, (dict, list)) for item in value) + ) + + +LeadingZeroStringDumper.add_representer(str, represent_string) +LeadingZeroStringDumper.add_representer(list, represent_list) + + +def save_yaml_file(path: Path, data: dict[str, Any]) -> None: + """Write enriched YAML with stable key order and safe zero-padded identifiers.""" + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8", newline="\n") as output_file: + yaml.dump(data, output_file, Dumper=LeadingZeroStringDumper, allow_unicode=True, sort_keys=False) diff --git a/source/mobileapp-mappings-2.0.yaml b/source/mobileapp-mappings-2.0.yaml index 333f54333..9fcc2ffd3 100644 --- a/source/mobileapp-mappings-2.0.yaml +++ b/source/mobileapp-mappings-2.0.yaml @@ -2,31 +2,33 @@ meta: edition: mobileapp component: mappings language: ALL - version: "2.0" + version: '2.0' layouts: [cards, leaflet] templates: [bridge_qr, bridge, tarot, tarot_qr] languages: [en, ru, uk] url_templates: - owasp_mastg: 'https://mas.owasp.org/MASTG-TEST-{code}' - owasp_mastg_best: 'https://mas.owasp.org/MASTG-BEST-{code}' - owasp_mastg_know: 'https://mas.owasp.org/MASTG-KNOW-{code}' - owasp_maswe: 'https://mas.owasp.org/MASWE-{code}' - capec: '/taxonomy/capec-3.9/{code}' - stride: '/taxonomy/stride/{code}' - safecode: 'https://safecode.org/publication/SAFECode_Agile_Dev_Security0712.pdf' + owasp_mastg: https://mas.owasp.org/MASTG-TEST-{code} + owasp_mastg_best: https://mas.owasp.org/MASTG-BEST-{code} + owasp_mastg_know: https://mas.owasp.org/MASTG-KNOW-{code} + owasp_maswe: https://mas.owasp.org/MASWE-{code} + owasp_masvs: https://mas.owasp.org/{code} + capec: /taxonomy/capec-3.9/{code} + stride: /taxonomy/stride/{code} + safecode: https://safecode.org/publication/SAFECode_Agile_Dev_Security0712.pdf labels: value: '' url: '' - stride: 'STRIDE' + stride: STRIDE stride_print: '' - owasp_mastg: 'MASTG' - owasp_mastg_best: 'MASTG Best' - owasp_mastg_know: 'MASTG Know' - owasp_maswe: 'MASWE' - capec: 'CAPEC™' - safecode: 'SAFECode™' - threat: 'MAS Threat' - attack_vector: 'MAS Attack' + owasp_mastg: MASTG + owasp_mastg_best: MASTG Best + owasp_mastg_know: MASTG Know + owasp_maswe: MASWE + owasp_masvs: MASVS + capec: CAPEC™ + safecode: SAFECode™ + threat: MAS Threat + attack_vector: MAS Attack suits: - id: PC name: Platform & Code @@ -34,8 +36,8 @@ suits: - id: PC2 value: '2' url: https://cornucopia.owasp.org/cards/PC2 - stride: [ I ] - stride_print: [ 'Information Disclosure' ] + stride: [I] + stride_print: [Information Disclosure] owasp_mastg: ['0289', '0290', '0291', '0292', '0293', '0294'] owasp_mastg_best: ['0014', '0015', '0017', '0018', '0033'] owasp_mastg_know: ['0053', '0099', '0076'] @@ -50,11 +52,12 @@ suits: '0071': Capturing or recording the screen from another app or an external tool. '0072': Accessing screenshots automatically taken by the system, e.g., when the app moves to the background. + owasp_masvs: [MASVS-PLATFORM-3, MASVS-STORAGE-2, MASVS-PLATFORM-2, MASVS-CODE-4] - id: PC3 value: '3' url: https://cornucopia.owasp.org/cards/PC3 - stride: [ ] - stride_print: [ ] + stride: [] + stride_print: [] owasp_mastg: ['0316', '0320', '0346', '0347', '0378', '0390'] owasp_mastg_best: ['0028', '0044', '0059', '0060', '0069'] owasp_mastg_know: ['0018', '0121', '0141', '0076', '0139', '0082'] @@ -84,6 +87,8 @@ suits: '0088': Holding excessive or no-longer-needed permissions granted to the app. '0089': Using permissions granted to the host app to call protected APIs and collect data from a third-party SDK. + owasp_masvs: [MASVS-PLATFORM-3, MASVS-STORAGE-2, MASVS-STORAGE-1, MASVS-CRYPTO-2, + MASVS-PLATFORM-2, MASVS-CODE-4, MASVS-PRIVACY-1] - id: PC4 value: '4' url: https://cornucopia.owasp.org/cards/PC4 @@ -102,6 +107,7 @@ suits: '0088': Holding excessive or no-longer-needed permissions granted to the app. '0089': Using permissions granted to the host app to call protected APIs and collect data from a third-party SDK. + owasp_masvs: [MASVS-PRIVACY-1] - id: PC5 value: '5' url: https://cornucopia.owasp.org/cards/PC5 @@ -124,6 +130,7 @@ suits: '0050': Modifying or replaying mutable PendingIntents obtained from the app. '0059': Supplying crafted input through any external interface (network, IPC, files, UI, or peripherals). + owasp_masvs: [MASVS-PLATFORM-1, MASVS-STORAGE-2, MASVS-CODE-4] - id: PC6 value: '6' url: https://cornucopia.owasp.org/cards/PC6 @@ -148,6 +155,7 @@ suits: '0049': Registering intent filters to intercept implicit intents sent by the app. '0050': Modifying or replaying mutable PendingIntents obtained from the app. + owasp_masvs: [MASVS-AUTH-1, MASVS-PLATFORM-1, MASVS-STORAGE-2] - id: PC7 value: '7' url: https://cornucopia.owasp.org/cards/PC7 @@ -171,6 +179,7 @@ suits: '0038': Invoking exported or unprotected app components from another app installed on the device. '0039': Connecting to open ports or local services exposed by the app. + owasp_masvs: [MASVS-CODE-4, MASVS-AUTH-1, MASVS-PLATFORM-1, MASVS-STORAGE-2] - id: PC8 value: '8' url: https://cornucopia.owasp.org/cards/PC8 @@ -189,6 +198,7 @@ suits: '0038': Invoking exported or unprotected app components from another app installed on the device. '0039': Connecting to open ports or local services exposed by the app. + owasp_masvs: [MASVS-AUTH-1, MASVS-PLATFORM-1, MASVS-STORAGE-2] - id: PC9 value: '9' url: https://cornucopia.owasp.org/cards/PC9 @@ -209,6 +219,8 @@ suits: '0059': Supplying crafted input through any external interface (network, IPC, files, UI, or peripherals). '0048': Capturing user input through a malicious custom keyboard or app extension. + owasp_masvs: [MASVS-CODE-4, MASVS-STORAGE-1, MASVS-STORAGE-2, MASVS-CRYPTO-2, + MASVS-PLATFORM-1] - id: PCX value: 10 url: https://cornucopia.owasp.org/cards/PCX @@ -241,6 +253,7 @@ suits: page. '0051': Injecting malicious JavaScript into WebView content (e.g., via MITM on insecure connections or a compromised website). + owasp_masvs: [MASVS-CODE-1, MASVS-CODE-3, MASVS-CODE-2, MASVS-PLATFORM-2, MASVS-CODE-4] - id: PCJ value: J url: https://cornucopia.owasp.org/cards/PCJ @@ -264,6 +277,7 @@ suits: on insecure connections or a compromised website). '0046': Registering the same custom URL scheme to intercept links intended for the app. + owasp_masvs: [MASVS-PLATFORM-2, MASVS-CODE-4, MASVS-STORAGE-2, MASVS-PLATFORM-1] - id: PCQ value: Q url: https://cornucopia.owasp.org/cards/PCQ @@ -304,6 +318,8 @@ suits: content providers. '0008': Extracting local or cloud backups of the device. '0048': Capturing user input through a malicious custom keyboard or app extension. + owasp_masvs: [MASVS-PLATFORM-2, MASVS-CODE-4, MASVS-STORAGE-2, MASVS-AUTH-1, MASVS-PLATFORM-1, + MASVS-STORAGE-1, MASVS-CRYPTO-2] - id: PCK value: K url: https://cornucopia.owasp.org/cards/PCK @@ -322,6 +338,7 @@ suits: '0001': Obtaining the app package and reverse engineering it. '0059': Supplying crafted input through any external interface (network, IPC, files, UI, or peripherals). + owasp_masvs: [MASVS-CODE-3, MASVS-CODE-4] - id: PCA value: A url: https://cornucopia.owasp.org/cards/PCA @@ -333,14 +350,15 @@ suits: owasp_maswe: [] capec: ['-'] safecode: ['-'] + owasp_masvs: [] - id: AA name: Authentication & Authorization cards: - id: AA2 value: '2' url: https://cornucopia.owasp.org/cards/AA2 - stride: [ S, I, E ] - stride_print: [ 'Spoofing', 'Information Disclosure', 'Elevation of Privilege' ] + stride: [S, I, E] + stride_print: [Spoofing, Information Disclosure, Elevation of Privilege] owasp_mastg: ['0266', '0267', '0268', '0269'] owasp_mastg_best: [] owasp_mastg_know: ['0056', '0057'] @@ -360,11 +378,12 @@ suits: '0040': Patching or repackaging the app to remove or alter client-side checks. '0034': Using a known, guessed, or shoulder-surfed device credential (PIN, pattern, or password). + owasp_masvs: [MASVS-AUTH-2, MASVS-CRYPTO-2] - id: AA3 value: '3' url: https://cornucopia.owasp.org/cards/AA3 - stride: [ T, I ] - stride_print: [ Tampering, 'Information Disclosure' ] + stride: [T, I] + stride_print: [Tampering, Information Disclosure] owasp_mastg: ['0266', '0267', '0268', '0269', '0270', '0271', '0326', '0327', '0328', '0329', '0330'] owasp_mastg_best: ['0031', '0036', '0037', '0038'] @@ -389,11 +408,12 @@ suits: or password). '0035': Enrolling additional biometrics on the device after obtaining the device credential. + owasp_masvs: [MASVS-AUTH-2, MASVS-CRYPTO-2] - id: AA4 value: '4' url: https://cornucopia.owasp.org/cards/AA4 - stride: [ S ] - stride_print: [ Spoofing ] + stride: [S] + stride_print: [Spoofing] owasp_mastg: ['0266', '0267', '0268', '0269', '0270', '0271', '0327', '0330'] owasp_mastg_best: ['0036'] owasp_mastg_know: ['0056', '0057', '0001', '0043', '0047', '0012'] @@ -417,11 +437,12 @@ suits: or password). '0035': Enrolling additional biometrics on the device after obtaining the device credential. + owasp_masvs: [MASVS-AUTH-2, MASVS-CRYPTO-2] - id: AA5 value: '5' url: https://cornucopia.owasp.org/cards/AA5 - stride: [ S ] - stride_print: [ Spoofing ] + stride: [S] + stride_print: [Spoofing] owasp_mastg: ['0340', '0370', '0371', '0381', '0393', '0394', '0395'] owasp_mastg_best: ['0040', '0045', '0054', '0055', '0063', '0070', '0071', '0072'] owasp_mastg_know: ['0022', '0079', '0019', '0080'] @@ -446,11 +467,12 @@ suits: '0049': Registering intent filters to intercept implicit intents sent by the app. '0050': Modifying or replaying mutable PendingIntents obtained from the app. + owasp_masvs: [MASVS-PLATFORM-3, MASVS-STORAGE-2, MASVS-PLATFORM-1, MASVS-CODE-4] - id: AA6 value: '6' url: https://cornucopia.owasp.org/cards/AA6 - stride: [ S, T ] - stride_print: [ Spoofing, Tampering ] + stride: [S, T] + stride_print: [Spoofing, Tampering] owasp_mastg: ['0266', '0267', '0268', '0269', '0326', '0327', '0329'] owasp_mastg_best: ['0031', '0036', '0038'] owasp_mastg_know: ['0056', '0057', '0001', '0043', '0047', '0012'] @@ -470,11 +492,12 @@ suits: '0040': Patching or repackaging the app to remove or alter client-side checks. '0034': Using a known, guessed, or shoulder-surfed device credential (PIN, pattern, or password). + owasp_masvs: [MASVS-AUTH-2, MASVS-CRYPTO-2] - id: AA7 value: '7' url: https://cornucopia.owasp.org/cards/AA7 - stride: [ T, E ] - stride_print: [ Tampering, 'Elevation of Privilege' ] + stride: [T, E] + stride_print: [Tampering, Elevation of Privilege] owasp_mastg: ['0266', '0267', '0327', '0329', '0375'] owasp_mastg_best: ['0036', '0038', '0057'] owasp_mastg_know: ['0056', '0057', '0001', '0043', '0047', '0012', '0025', '0138'] @@ -495,11 +518,12 @@ suits: page. '0059': Supplying crafted input through any external interface (network, IPC, files, UI, or peripherals). + owasp_masvs: [MASVS-AUTH-2, MASVS-CRYPTO-2, MASVS-CODE-4] - id: AA8 value: '8' url: https://cornucopia.owasp.org/cards/AA8 - stride: [ E ] - stride_print: [ 'Elevation of Privilege' ] + stride: [E] + stride_print: [Elevation of Privilege] owasp_mastg: ['0266', '0267', '0327'] owasp_mastg_best: ['0036'] owasp_mastg_know: ['0056', '0057', '0001', '0043', '0047', '0012'] @@ -515,11 +539,12 @@ suits: '0027': Invoking keystore operations on a compromised or stolen device when key use does not require user authentication. '0040': Patching or repackaging the app to remove or alter client-side checks. + owasp_masvs: [MASVS-AUTH-2, MASVS-CRYPTO-2] - id: AA9 value: '9' url: https://cornucopia.owasp.org/cards/AA9 - stride: [ E ] - stride_print: [ 'Elevation of Privilege' ] + stride: [E] + stride_print: [Elevation of Privilege] owasp_mastg: ['0250', '0251', '0252', '0253', '0254', '0335', '0336', '0360', '0361', '0362', '0363'] owasp_mastg_best: ['0011', '0012', '0013', '0049', '0010', '0033', '0051'] @@ -539,11 +564,12 @@ suits: '0088': Holding excessive or no-longer-needed permissions granted to the app. '0089': Using permissions granted to the host app to call protected APIs and collect data from a third-party SDK. + owasp_masvs: [MASVS-PLATFORM-2, MASVS-STORAGE-2, MASVS-CODE-4, MASVS-PRIVACY-1] - id: AAX value: 10 url: https://cornucopia.owasp.org/cards/AAX - stride: [ S, I, E ] - stride_print: [ Spoofing, 'Information Disclosure', 'Elevation of Privilege' ] + stride: [S, I, E] + stride_print: [Spoofing, Information Disclosure, Elevation of Privilege] owasp_mastg: ['0266', '0267', '0268', '0269', '0270', '0271', '0326', '0327', '0328', '0329', '0330'] owasp_mastg_best: ['0031', '0036', '0037', '0038'] @@ -568,11 +594,12 @@ suits: or password). '0035': Enrolling additional biometrics on the device after obtaining the device credential. + owasp_masvs: [MASVS-AUTH-2, MASVS-CRYPTO-2] - id: AAJ value: J url: https://cornucopia.owasp.org/cards/AAJ - stride: [ E ] - stride_print: [ 'Elevation of Privilege' ] + stride: [E] + stride_print: [Elevation of Privilege] owasp_mastg: ['0266', '0267', '0268', '0269', '0270', '0271', '0327', '0330', '0364', '0365', '0366'] owasp_mastg_best: ['0036', '0052'] @@ -603,11 +630,12 @@ suits: '0038': Invoking exported or unprotected app components from another app installed on the device. '0039': Connecting to open ports or local services exposed by the app. + owasp_masvs: [MASVS-AUTH-2, MASVS-CRYPTO-2, MASVS-AUTH-1, MASVS-PLATFORM-1, MASVS-STORAGE-2] - id: AAQ value: Q url: https://cornucopia.owasp.org/cards/AAQ - stride: [ T, I, E ] - stride_print: [ Tampering, 'Information Disclosure', 'Elevation of Privilege' ] + stride: [T, I, E] + stride_print: [Tampering, Information Disclosure, Elevation of Privilege] owasp_mastg: ['0334', '0339', '0376', '0377', '0379', '0380'] owasp_mastg_best: ['0011', '0012', '0013', '0035', '0039', '0058', '0062', '0061', '0059'] @@ -625,11 +653,12 @@ suits: on insecure connections or a compromised website). '0059': Supplying crafted input through any external interface (network, IPC, files, UI, or peripherals). + owasp_masvs: [MASVS-PLATFORM-2, MASVS-STORAGE-2, MASVS-CODE-4] - id: AAK value: K url: https://cornucopia.owasp.org/cards/AAK - stride: [ S, T, E ] - stride_print: [ Spoofing, Tampering, 'Elevation of Privilege' ] + stride: [S, T, E] + stride_print: [Spoofing, Tampering, Elevation of Privilege] owasp_mastg: [] owasp_mastg_best: [] owasp_mastg_know: ['0266', '0267', '0268', '0269', '0270', '0271', '0326', '0327', @@ -637,25 +666,27 @@ suits: owasp_maswe: [] capec: [114, 115, 207, 554] safecode: [8, 10, 11] + owasp_masvs: [] - id: AAA value: A url: https://cornucopia.owasp.org/cards/AAA - stride: [ S, E ] - stride_print: [ 'Spoofing', 'Elevation of Privilege' ] + stride: [S, E] + stride_print: [Spoofing, Elevation of Privilege] owasp_mastg: ['-'] owasp_mastg_best: [] owasp_mastg_know: [] owasp_maswe: [] capec: ['-'] safecode: ['-'] + owasp_masvs: [] - id: NS name: Network & Storage cards: - id: NS2 value: '2' url: https://cornucopia.owasp.org/cards/NS2 - stride: [ I ] - stride_print: [ 'Information Disclosure' ] + stride: [I] + stride_print: [Information Disclosure] owasp_mastg: ['0203', '0231', '0296', '0297'] owasp_mastg_best: ['0002', '0022'] owasp_mastg_know: ['0049', '0101'] @@ -668,11 +699,12 @@ suits: '0005': Accessing the device storage on a compromised device. '0006': Accessing the system logs on a compromised device or from an app holding log-access permissions. + owasp_masvs: [MASVS-STORAGE-2] - id: NS3 value: '3' url: https://cornucopia.owasp.org/cards/NS3 - stride: [ I ] - stride_print: [ 'Information Disclosure' ] + stride: [I] + stride_print: [Information Disclosure] owasp_mastg: ['0258', '0276', '0277', '0278', '0279', '0280', '0313', '0314'] owasp_mastg_best: ['0019', '0026'] owasp_mastg_know: ['0055', '0100', '0083'] @@ -688,6 +720,7 @@ suits: apps. '0043': Observing the device screen while sensitive data is displayed or entered (shoulder surfing). + owasp_masvs: [MASVS-PLATFORM-3, MASVS-STORAGE-2] - id: NS4 value: '4' url: https://cornucopia.owasp.org/cards/NS4 @@ -712,6 +745,7 @@ suits: the device. '0045': Reading notification content from another app holding notification-access permissions. + owasp_masvs: [MASVS-PRIVACY-3, MASVS-PRIVACY-1, MASVS-PLATFORM-3, MASVS-STORAGE-2] - id: NS5 value: '5' url: https://cornucopia.owasp.org/cards/NS5 @@ -739,6 +773,7 @@ suits: '0008': Extracting local or cloud backups of the device. '0009': Tampering with backup contents and restoring the modified backup to a device. + owasp_masvs: [MASVS-STORAGE-1, MASVS-STORAGE-2, MASVS-CRYPTO-2] - id: NS6 value: '6' url: https://cornucopia.owasp.org/cards/NS6 @@ -755,6 +790,7 @@ suits: and use its keys when no device credential protects them. attack_vector: '0063': Accessing a lost or stolen device that has no secure lock screen configured. + owasp_masvs: [MASVS-CRYPTO-2] - id: NS7 value: '7' url: https://cornucopia.owasp.org/cards/NS7 @@ -766,6 +802,7 @@ suits: owasp_maswe: [] capec: [679] safecode: ['-'] + owasp_masvs: [] - id: NS8 value: '8' url: https://cornucopia.owasp.org/cards/NS8 @@ -795,6 +832,8 @@ suits: '0009': Tampering with backup contents and restoring the modified backup to a device. '0070': Modifying the app's files or resources on a compromised device. + owasp_masvs: [MASVS-STORAGE-1, MASVS-STORAGE-2, MASVS-CRYPTO-2, MASVS-RESILIENCE-2, + MASVS-CODE-4] - id: NS9 value: '9' url: https://cornucopia.owasp.org/cards/NS9 @@ -812,6 +851,7 @@ suits: '0009': Tampering with backup contents and restoring the modified backup to a device. '0070': Modifying the app's files or resources on a compromised device. + owasp_masvs: [MASVS-RESILIENCE-2, MASVS-CODE-4] - id: NSX value: '10' url: https://cornucopia.owasp.org/cards/NSX @@ -833,6 +873,7 @@ suits: coerced, or rogue Certificate Authority (CA). '0017': Installing an attacker-controlled CA certificate on a device they control to inspect the app's traffic. + owasp_masvs: [MASVS-NETWORK-2] - id: NSJ value: J url: https://cornucopia.owasp.org/cards/NSJ @@ -857,6 +898,7 @@ suits: DNS spoofing, or a rogue access point. '0015': Presenting a fraudulent or otherwise invalid certificate that the app accepts. + owasp_masvs: [MASVS-NETWORK-1] - id: NSQ value: Q url: https://cornucopia.owasp.org/cards/NSQ @@ -877,6 +919,7 @@ suits: USB. '0014': Performing a Machine-in-the-Middle (MITM) attack, e.g., via ARP poisoning, DNS spoofing, or a rogue access point. + owasp_masvs: [MASVS-NETWORK-1] - id: NSK value: K url: https://cornucopia.owasp.org/cards/NSK @@ -895,6 +938,7 @@ suits: DNS spoofing, or a rogue access point. '0015': Presenting a fraudulent or otherwise invalid certificate that the app accepts. + owasp_masvs: [MASVS-NETWORK-1] - id: NSA value: A url: https://cornucopia.owasp.org/cards/NSA @@ -906,6 +950,7 @@ suits: owasp_maswe: [] capec: ['-'] safecode: ['-'] + owasp_masvs: [] - id: RS name: Resilience cards: @@ -926,6 +971,7 @@ suits: '0001': Obtaining the app package and reverse engineering it. '0006': Accessing the system logs on a compromised device or from an app holding log-access permissions. + owasp_masvs: [MASVS-RESILIENCE-3] - id: RS3 value: '3' url: https://cornucopia.owasp.org/cards/RS3 @@ -943,6 +989,7 @@ suits: '0001': Obtaining the app package and reverse engineering it. '0006': Accessing the system logs on a compromised device or from an app holding log-access permissions. + owasp_masvs: [MASVS-RESILIENCE-3] - id: RS4 value: '4' url: https://cornucopia.owasp.org/cards/RS4 @@ -961,6 +1008,7 @@ suits: '0068': Impersonating the app with scripts, bots, or tampered clients when interacting with the backend. '0069': Installing a repackaged version of the app on victim devices. + owasp_masvs: [MASVS-RESILIENCE-2] - id: RS5 value: '5' url: https://cornucopia.owasp.org/cards/RS5 @@ -979,6 +1027,7 @@ suits: '0002': Debugging the app at runtime. '0003': Using dynamic instrumentation. '0004': Attaching a remote inspector to the app's debuggable web content. + owasp_masvs: [MASVS-RESILIENCE-4, MASVS-PLATFORM-2] - id: RS6 value: '6' url: https://cornucopia.owasp.org/cards/RS6 @@ -994,6 +1043,7 @@ suits: '0064': Attackers can inspect and manipulate the running app without resistance. attack_vector: '0002': Debugging the app at runtime. + owasp_masvs: [MASVS-RESILIENCE-4] - id: RS7 value: '7' url: https://cornucopia.owasp.org/cards/RS7 @@ -1010,6 +1060,7 @@ suits: attack_vector: '0003': Using dynamic instrumentation. '0066': Running the app in an emulator or virtual device. + owasp_masvs: [MASVS-RESILIENCE-1, MASVS-RESILIENCE-4] - id: RS8 value: '8' url: https://cornucopia.owasp.org/cards/RS8 @@ -1026,6 +1077,7 @@ suits: attack_vector: '0002': Debugging the app at runtime. '0003': Using dynamic instrumentation. + owasp_masvs: [MASVS-RESILIENCE-2] - id: RS9 value: '9' url: https://cornucopia.owasp.org/cards/RS9 @@ -1042,6 +1094,7 @@ suits: effort. attack_vector: '0001': Obtaining the app package and reverse engineering it. + owasp_masvs: [MASVS-RESILIENCE-3] - id: RSX value: '10' url: https://cornucopia.owasp.org/cards/RSX @@ -1060,6 +1113,7 @@ suits: '0003': Using dynamic instrumentation. '0005': Accessing the device storage on a compromised device. '0065': Running the app on a rooted or jailbroken device they control. + owasp_masvs: [MASVS-RESILIENCE-1, MASVS-RESILIENCE-4] - id: RSJ value: J url: https://cornucopia.owasp.org/cards/RSJ @@ -1077,6 +1131,7 @@ suits: '0009': Tampering with backup contents and restoring the modified backup to a device. '0070': Modifying the app's files or resources on a compromised device. + owasp_masvs: [MASVS-RESILIENCE-2, MASVS-CODE-4] - id: RSQ value: Q url: https://cornucopia.owasp.org/cards/RSQ @@ -1093,6 +1148,7 @@ suits: attack_vector: '0002': Debugging the app at runtime. '0003': Using dynamic instrumentation. + owasp_masvs: [MASVS-RESILIENCE-2] - id: RSK value: K url: https://cornucopia.owasp.org/cards/RSK @@ -1109,6 +1165,7 @@ suits: attack_vector: '0002': Debugging the app at runtime. '0003': Using dynamic instrumentation. + owasp_masvs: [MASVS-RESILIENCE-2] - id: RSA value: A url: https://cornucopia.owasp.org/cards/RSA @@ -1120,6 +1177,7 @@ suits: owasp_maswe: [] capec: ['-'] safecode: ['-'] + owasp_masvs: [] - id: CRM name: Cryptography cards: @@ -1143,6 +1201,7 @@ suits: recover plaintext. '0023': Exploiting padding oracles exposed through observable error signals or timing differences. + owasp_masvs: [MASVS-CRYPTO-1, MASVS-CRYPTO-2] - id: CRM3 value: '3' url: https://cornucopia.owasp.org/cards/CRM3 @@ -1162,6 +1221,7 @@ suits: output. '0024': Observing enough outputs to recover the internal state of a non-cryptographic PRNG. + owasp_masvs: [MASVS-CRYPTO-1] - id: CRM4 value: '4' url: https://cornucopia.owasp.org/cards/CRM4 @@ -1185,6 +1245,7 @@ suits: PRNG. '0018': Brute-forcing cryptographic material generated with insufficient length. '0020': Intercepting cryptographic keys exported in plaintext. + owasp_masvs: [MASVS-CRYPTO-1, MASVS-CRYPTO-2] - id: CRM5 value: '5' url: https://cornucopia.owasp.org/cards/CRM5 @@ -1213,6 +1274,7 @@ suits: '0023': Exploiting padding oracles exposed through observable error signals or timing differences. '0028': Crafting collisions or second preimages for broken hash functions. + owasp_masvs: [MASVS-CRYPTO-2, MASVS-CRYPTO-1] - id: CRM6 value: '6' url: https://cornucopia.owasp.org/cards/CRM6 @@ -1235,6 +1297,7 @@ suits: DNS spoofing, or a rogue access point. '0015': Presenting a fraudulent or otherwise invalid certificate that the app accepts. + owasp_masvs: [MASVS-RESILIENCE-2, MASVS-CODE-4, MASVS-NETWORK-1] - id: CRM7 value: '7' url: https://cornucopia.owasp.org/cards/CRM7 @@ -1259,6 +1322,7 @@ suits: '0007': Accessing files exposed through incorrect file permissions or misconfigured content providers. '0008': Extracting local or cloud backups of the device. + owasp_masvs: [MASVS-STORAGE-1, MASVS-STORAGE-2, MASVS-CRYPTO-2] - id: CRM8 value: '8' url: https://cornucopia.owasp.org/cards/CRM8 @@ -1288,6 +1352,7 @@ suits: '0023': Exploiting padding oracles exposed through observable error signals or timing differences. '0028': Crafting collisions or second preimages for broken hash functions. + owasp_masvs: [MASVS-CRYPTO-2, MASVS-CRYPTO-1] - id: CRM9 value: '9' url: https://cornucopia.owasp.org/cards/CRM9 @@ -1308,6 +1373,7 @@ suits: recover plaintext. '0023': Exploiting padding oracles exposed through observable error signals or timing differences. + owasp_masvs: [MASVS-CRYPTO-1, MASVS-CRYPTO-2] - id: CRMX value: '10' url: https://cornucopia.owasp.org/cards/CRMX @@ -1336,6 +1402,7 @@ suits: content providers. '0008': Extracting local or cloud backups of the device. '0001': Obtaining the app package and reverse engineering it. + owasp_masvs: [MASVS-STORAGE-1, MASVS-STORAGE-2, MASVS-CRYPTO-2] - id: CRMJ value: J url: https://cornucopia.owasp.org/cards/CRMJ @@ -1354,6 +1421,7 @@ suits: DNS spoofing, or a rogue access point. '0015': Presenting a fraudulent or otherwise invalid certificate that the app accepts. + owasp_masvs: [MASVS-NETWORK-1] - id: CRMQ value: Q url: https://cornucopia.owasp.org/cards/CRMQ @@ -1365,6 +1433,7 @@ suits: owasp_maswe: [] capec: [20, 116, 117, 97, 112, 485] safecode: [14, 21, 29, 32, 33] + owasp_masvs: [] - id: CRMK value: K url: https://cornucopia.owasp.org/cards/CRMK @@ -1381,6 +1450,7 @@ suits: attack_vector: '0002': Debugging the app at runtime. '0003': Using dynamic instrumentation. + owasp_masvs: [MASVS-RESILIENCE-2] - id: CRMA value: A url: https://cornucopia.owasp.org/cards/CRMA @@ -1392,6 +1462,7 @@ suits: owasp_maswe: [] capec: ['-'] safecode: ['-'] + owasp_masvs: [] - id: CM name: Cornucopia cards: @@ -1425,6 +1496,7 @@ suits: '0088': Holding excessive or no-longer-needed permissions granted to the app. '0089': Using permissions granted to the host app to call protected APIs and collect data from a third-party SDK. + owasp_masvs: [MASVS-PRIVACY-3, MASVS-PRIVACY-1] - id: CM3 value: '3' url: https://cornucopia.owasp.org/cards/CM3 @@ -1436,6 +1508,7 @@ suits: owasp_maswe: ['0076'] capec: [410] safecode: ['-'] + owasp_masvs: [] - id: CM4 value: '4' url: https://cornucopia.owasp.org/cards/CM4 @@ -1461,6 +1534,7 @@ suits: and services. '0075': Contacting undeclared tracking domains that platform enforcement cannot block. + owasp_masvs: [MASVS-PRIVACY-3, MASVS-PRIVACY-1] - id: CM5 value: '5' url: https://cornucopia.owasp.org/cards/CM5 @@ -1480,6 +1554,7 @@ suits: privacy labels. '0082': Transmitting undeclared identifiers or analytics data to first- or third-party services over the network. + owasp_masvs: [MASVS-PRIVACY-3, MASVS-PRIVACY-1] - id: CM6 value: '6' url: https://cornucopia.owasp.org/cards/CM6 @@ -1504,6 +1579,7 @@ suits: '0088': Holding excessive or no-longer-needed permissions granted to the app. '0089': Using permissions granted to the host app to call protected APIs and collect data from a third-party SDK. + owasp_masvs: [MASVS-PRIVACY-3, MASVS-PRIVACY-1] - id: CM7 value: '7' url: https://cornucopia.owasp.org/cards/CM7 @@ -1522,6 +1598,7 @@ suits: the app. '0047': Delivering crafted deep links or intents from a malicious app or web page. + owasp_masvs: [MASVS-PLATFORM-1, MASVS-STORAGE-2, MASVS-CODE-4] - id: CM8 value: '8' url: https://cornucopia.owasp.org/cards/CM8 @@ -1544,6 +1621,7 @@ suits: '0049': Registering intent filters to intercept implicit intents sent by the app. '0050': Modifying or replaying mutable PendingIntents obtained from the app. + owasp_masvs: [MASVS-PLATFORM-1, MASVS-STORAGE-2, MASVS-CODE-4] - id: CM9 value: 9 url: https://cornucopia.owasp.org/cards/CM9 @@ -1555,6 +1633,7 @@ suits: owasp_maswe: [] capec: [23, 165, 442] safecode: ['-'] + owasp_masvs: [] - id: CMX value: '10' url: https://cornucopia.owasp.org/cards/CMX @@ -1573,6 +1652,7 @@ suits: the app. '0047': Delivering crafted deep links or intents from a malicious app or web page. + owasp_masvs: [MASVS-PLATFORM-1, MASVS-STORAGE-2, MASVS-CODE-4] - id: CMJ value: J url: https://cornucopia.owasp.org/cards/CMJ @@ -1584,6 +1664,7 @@ suits: owasp_maswe: [] capec: [92, 100] safecode: [3, 6, 36] + owasp_masvs: [] - id: CMQ value: Q url: https://cornucopia.owasp.org/cards/CMQ @@ -1605,6 +1686,7 @@ suits: '0069': Installing a repackaged version of the app on victim devices. '0002': Debugging the app at runtime. '0003': Using dynamic instrumentation. + owasp_masvs: [MASVS-RESILIENCE-2] - id: CMK value: K url: https://cornucopia.owasp.org/cards/CMK @@ -1623,6 +1705,7 @@ suits: page. '0059': Supplying crafted input through any external interface (network, IPC, files, UI, or peripherals). + owasp_masvs: [MASVS-CODE-4] - id: CMA value: A url: https://cornucopia.owasp.org/cards/CMA @@ -1634,6 +1717,7 @@ suits: owasp_maswe: [] capec: ['-'] safecode: ['-'] + owasp_masvs: [] - id: WC name: WILD CARD cards: @@ -1648,6 +1732,7 @@ suits: owasp_maswe: [] capec: ['-'] safecode: ['-'] + owasp_masvs: [] - id: JOBM value: B url: https://cornucopia.owasp.org/cards/JOBM @@ -1691,3 +1776,5 @@ suits: '0049': Registering intent filters to intercept implicit intents sent by the app. '0050': Modifying or replaying mutable PendingIntents obtained from the app. + owasp_masvs: [MASVS-PRIVACY-3, MASVS-PRIVACY-1, MASVS-AUTH-1, MASVS-PLATFORM-1, + MASVS-STORAGE-2] diff --git a/tests/scripts/enrich_mobileapp_mappings_utest.py b/tests/scripts/enrich_mobileapp_mappings_utest.py index ec7f20347..4e12eba4f 100644 --- a/tests/scripts/enrich_mobileapp_mappings_utest.py +++ b/tests/scripts/enrich_mobileapp_mappings_utest.py @@ -27,6 +27,7 @@ def test_enrich_mappings_merges_mastg_siblings_and_maswe_metadata(self) -> None: mastg = {"0357": {"owasp_maswe": ["0018"], "owasp_mastg_know": ["0020", "0117"], "owasp_mastg_best": ["0049"]}} maswe = { "0018": { + "owasp_masvs": ["MASVS-STORAGE-1", "MASVS-PLATFORM-3"], "owasp_mas_threat": { "0018": "Attackers can access sensitive data and functionality exposed by app components." }, @@ -43,9 +44,59 @@ def test_enrich_mappings_merges_mastg_siblings_and_maswe_metadata(self) -> None: self.assertEqual(["0049"], card["owasp_mastg_best"]) self.assertEqual(["0104", "0020", "0117"], card["owasp_mastg_know"]) self.assertEqual(["0018"], card["owasp_maswe"]) + self.assertEqual(["MASVS-STORAGE-1", "MASVS-PLATFORM-3"], card["owasp_masvs"]) self.assertEqual(maswe["0018"]["owasp_mas_attack"], card["attack_vector"]) self.assertEqual(maswe["0018"]["owasp_mas_threat"], card["threat"]) + def test_enrich_mappings_derives_masvs_from_existing_maswe_values(self) -> None: + mappings = { + "suits": [ + { + "cards": [ + { + "id": "NSX", + "owasp_mastg": ["-"], + "owasp_maswe": ["0001", "9999"], + } + ] + } + ] + } + maswe = {"0001": {"owasp_masvs": ["MASVS-STORAGE-1"]}} + + with self.assertLogs(level="WARNING"): + card = enricher.enrich_mappings(mappings, {}, maswe)["suits"][0]["cards"][0] + + self.assertEqual(["0001", "9999"], card["owasp_maswe"]) + self.assertEqual(["MASVS-STORAGE-1"], card["owasp_masvs"]) + + def test_enrich_mappings_replaces_stale_masvs_values(self) -> None: + mappings = { + "suits": [ + { + "cards": [ + { + "id": "NSY", + "owasp_mastg": ["-"], + "owasp_maswe": ["0001"], + "owasp_masvs": ["MASVS-OLD"], + } + ] + } + ] + } + maswe = {"0001": {"owasp_masvs": ["MASVS-STORAGE-1"]}} + + card = enricher.enrich_mappings(mappings, {}, maswe)["suits"][0]["cards"][0] + + self.assertEqual(["MASVS-STORAGE-1"], card["owasp_masvs"]) + + def test_enrich_mappings_rejects_invalid_masvs_metadata(self) -> None: + mappings = {"suits": [{"cards": [{"id": "PC1", "owasp_mastg": ["-"], "owasp_maswe": ["0001"]}]}]} + + with self.assertRaisesRegex(ValueError, "MASWE 0001 owasp_masvs"): + enricher.enrich_mappings(mappings, {}, {"0001": {"owasp_masvs": "MASVS-STORAGE-1"}}) + def test_enrich_mappings_preserves_placeholder_mastg_value(self) -> None: mappings = {"suits": [{"cards": [{"id": "PCA", "owasp_mastg": ["-"]}]}]} @@ -53,6 +104,7 @@ def test_enrich_mappings_preserves_placeholder_mastg_value(self) -> None: self.assertEqual(["-"], card["owasp_mastg"]) self.assertEqual([], card["owasp_maswe"]) + self.assertEqual([], card["owasp_masvs"]) def test_enrich_mappings_preserves_legacy_mastg_value(self) -> None: mappings = {"suits": [{"cards": [{"id": "CMJ", "owasp_mastg": ["0043"]}]}]} @@ -85,6 +137,9 @@ def test_load_yaml_file_rejects_duplicate_keys(self) -> None: def test_load_yaml_file_validates_size_and_document_type(self) -> None: with tempfile.TemporaryDirectory() as directory: input_path = Path(directory) / "mapping.yaml" + input_path.write_text("suits: []\n", encoding="utf-8") + self.assertEqual({"suits": []}, enricher.load_yaml_file(input_path)) + input_path.write_text("[]\n", encoding="utf-8") with self.assertRaisesRegex(ValueError, "expected a mapping"): enricher.load_yaml_file(input_path) @@ -115,6 +170,8 @@ def test_enrich_mappings_rejects_invalid_structure_and_metadata(self) -> None: ) def test_parse_arguments_rejects_traversal_and_main_uses_default_paths(self) -> None: + self.assertEqual("mobileapp", enricher.parse_arguments([]).edition) + with self.assertRaises(SystemExit): enricher.parse_arguments(["--edition", "../outside"])