attackmap 0.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. attackmap/__init__.py +2 -0
  2. attackmap/analyzer.py +449 -0
  3. attackmap/analyzer_contracts.py +194 -0
  4. attackmap/analyzers.py +1003 -0
  5. attackmap/anomalies.py +612 -0
  6. attackmap/asset_model.py +245 -0
  7. attackmap/attack_taxonomy.py +133 -0
  8. attackmap/auth_protocol.py +702 -0
  9. attackmap/authz.py +1695 -0
  10. attackmap/bench.py +467 -0
  11. attackmap/cli.py +1474 -0
  12. attackmap/config_scanner.py +389 -0
  13. attackmap/context_pack.py +82 -0
  14. attackmap/contracts.py +201 -0
  15. attackmap/control_model.py +280 -0
  16. attackmap/crossrepo.py +356 -0
  17. attackmap/crypto.py +240 -0
  18. attackmap/cve.py +717 -0
  19. attackmap/defensive_review.py +647 -0
  20. attackmap/detection_opportunities.py +1221 -0
  21. attackmap/detections/__init__.py +491 -0
  22. attackmap/detections/bola.yml +57 -0
  23. attackmap/detections/cloud.yml +609 -0
  24. attackmap/detections/injection.yml +167 -0
  25. attackmap/detections/secrets.yml +48 -0
  26. attackmap/detections/sigma.py +511 -0
  27. attackmap/detections/ssrf.yml +98 -0
  28. attackmap/detections/weakness.yml +62 -0
  29. attackmap/detections/webhook.yml +45 -0
  30. attackmap/detections/workflow.yml +121 -0
  31. attackmap/diagrams.py +237 -0
  32. attackmap/diff.py +301 -0
  33. attackmap/exploitability.py +493 -0
  34. attackmap/fingerprint.py +91 -0
  35. attackmap/fleet.py +370 -0
  36. attackmap/gitref.py +129 -0
  37. attackmap/graph.py +26 -0
  38. attackmap/handler_auth.py +639 -0
  39. attackmap/handler_span.py +62 -0
  40. attackmap/hunt_harness.py +505 -0
  41. attackmap/insights.py +701 -0
  42. attackmap/llm_review.py +846 -0
  43. attackmap/lockfiles.py +521 -0
  44. attackmap/mass_assignment.py +543 -0
  45. attackmap/md.py +175 -0
  46. attackmap/merge.py +333 -0
  47. attackmap/models.py +1284 -0
  48. attackmap/plugins_lock.py +114 -0
  49. attackmap/popular_packages.py +193 -0
  50. attackmap/progress.py +258 -0
  51. attackmap/recon_models.py +31 -0
  52. attackmap/recon_to_analysis.py +149 -0
  53. attackmap/redact.py +220 -0
  54. attackmap/report.py +424 -0
  55. attackmap/review_eval.py +335 -0
  56. attackmap/review_json.py +308 -0
  57. attackmap/review_prompts.py +796 -0
  58. attackmap/route_auth_fusion.py +474 -0
  59. attackmap/safe_fs.py +285 -0
  60. attackmap/sarif.py +373 -0
  61. attackmap/sbom.py +418 -0
  62. attackmap/scanner.py +1379 -0
  63. attackmap/sdk/__init__.py +278 -0
  64. attackmap/sdk/contracts.py +17 -0
  65. attackmap/sdk/fs.py +156 -0
  66. attackmap/sdk/models.py +59 -0
  67. attackmap/secrets_history.py +161 -0
  68. attackmap/security_overlay.py +71 -0
  69. attackmap/srcpaths.py +301 -0
  70. attackmap/suggest.py +394 -0
  71. attackmap/supply_chain.py +688 -0
  72. attackmap/suppress.py +740 -0
  73. attackmap/taint.py +1540 -0
  74. attackmap/taint_flow.py +1419 -0
  75. attackmap/taint_sinks.py +431 -0
  76. attackmap/taint_sinks.yaml +308 -0
  77. attackmap/taint_sources.py +396 -0
  78. attackmap/taxonomy.py +465 -0
  79. attackmap/threat_model.py +3441 -0
  80. attackmap/topology.py +316 -0
  81. attackmap/triage.py +151 -0
  82. attackmap/weaknesses.py +365 -0
  83. attackmap/webhardening.py +479 -0
  84. attackmap/webhook_verification.py +1067 -0
  85. attackmap/workflow_scanner.py +964 -0
  86. attackmap-0.6.4.dist-info/METADATA +226 -0
  87. attackmap-0.6.4.dist-info/RECORD +91 -0
  88. attackmap-0.6.4.dist-info/WHEEL +5 -0
  89. attackmap-0.6.4.dist-info/entry_points.txt +2 -0
  90. attackmap-0.6.4.dist-info/licenses/LICENSE +21 -0
  91. attackmap-0.6.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,114 @@
1
+ """Official analyzer plugins, pinned to immutable commits (#237).
2
+
3
+ Generated by scripts/update_plugin_lock.py — do not edit by hand. Only
4
+ these may be auto-installed by `-m` / `suggest --install`, and only at
5
+ the pinned commit.
6
+ """
7
+
8
+ OFFICIAL_PLUGINS: list[dict] = [
9
+ {
10
+ 'repo': 'attackmap-analyzer-python',
11
+ 'package': 'attackmap-analyzer-python',
12
+ 'version': '0.2.0',
13
+ 'git_sha': '9888be5c8f8e9127686e0ed98af9425a34bc7b8c',
14
+ 'entry_points': ['python'],
15
+ },
16
+ {
17
+ 'repo': 'attackmap-analyzer-rust',
18
+ 'package': 'attackmap-analyzer-rust',
19
+ 'version': '0.2.0',
20
+ 'git_sha': 'ba6cce7ec1b8055b27c3aa2ddd8884ae410b8ba0',
21
+ 'entry_points': ['rust'],
22
+ },
23
+ {
24
+ 'repo': 'attackmap-analyzer-go',
25
+ 'package': 'attackmap-analyzer-go',
26
+ 'version': '0.2.0',
27
+ 'git_sha': '8570338fc67198770b3185633bb0fc35d7db5deb',
28
+ 'entry_points': ['go'],
29
+ },
30
+ {
31
+ 'repo': 'attackmap-analyzer-java-spring',
32
+ 'package': 'attackmap-analyzer-java-spring',
33
+ 'version': '0.2.0',
34
+ 'git_sha': '82e23461f6c075d17a709c29c9349a1076d1bafa',
35
+ 'entry_points': ['java_spring'],
36
+ },
37
+ {
38
+ 'repo': 'attackmap-analyzer-dotnet',
39
+ 'package': 'attackmap-analyzer-dotnet',
40
+ 'version': '0.2.0',
41
+ 'git_sha': 'fd08a2899f81c7173e257e892fb3914704244f3d',
42
+ 'entry_points': ['dotnet'],
43
+ },
44
+ {
45
+ 'repo': 'attackmap-analyzer-terraform',
46
+ 'package': 'attackmap-analyzer-terraform',
47
+ 'version': '0.2.1',
48
+ 'git_sha': '1dfbce9a32c9a2fd6fc1b80f976ea86f69d82cd1',
49
+ 'entry_points': ['terraform'],
50
+ },
51
+ {
52
+ 'repo': 'attackmap-analyzer-c',
53
+ 'package': 'attackmap-analyzer-c',
54
+ 'version': '0.2.0',
55
+ 'git_sha': '00f7615a75df47e4ea1a084af562b4cf2e037ef1',
56
+ 'entry_points': ['c'],
57
+ },
58
+ {
59
+ 'repo': 'attackmap-analyzer-cpp',
60
+ 'package': 'attackmap-analyzer-cpp',
61
+ 'version': '0.2.0',
62
+ 'git_sha': 'cae9511e8db3ae192af9189a5d15d889e7fd18a8',
63
+ 'entry_points': ['cpp'],
64
+ },
65
+ {
66
+ 'repo': 'attackmap-analyzer-swift',
67
+ 'package': 'attackmap-analyzer-swift',
68
+ 'version': '0.2.0',
69
+ 'git_sha': 'eb56ca09c642e580c5cd73391d602f555d429a4b',
70
+ 'entry_points': ['objc', 'swift'],
71
+ },
72
+ {
73
+ 'repo': 'attackmap-analyzer-node-service',
74
+ 'package': 'attackmap-analyzer-node-service',
75
+ 'version': '0.3.0',
76
+ 'git_sha': '949269777c020e7a6265fb1436cf1be2fe5004be',
77
+ 'entry_points': ['node_service'],
78
+ },
79
+ {
80
+ 'repo': 'attackmap-analyzer-atproto',
81
+ 'package': 'attackmap-analyzer-atproto',
82
+ 'version': '0.2.0',
83
+ 'git_sha': '34b7616d5428549d39848dc4db1af7a71d6e0b56',
84
+ 'entry_points': ['atproto'],
85
+ },
86
+ {
87
+ 'repo': 'attackmap-analyzer-php-web',
88
+ 'package': 'attackmap-analyzer-php-web',
89
+ 'version': '0.2.0',
90
+ 'git_sha': '65a5f098c40ae64d3ec3b5d20777eac640fb743b',
91
+ 'entry_points': ['php_web'],
92
+ },
93
+ {
94
+ 'repo': 'attackmap-analyzer-php-laminas',
95
+ 'package': 'attackmap-analyzer-php-laminas',
96
+ 'version': '0.2.0',
97
+ 'git_sha': '9046ff6256338850574ee781b69bd2e3f09d2ab2',
98
+ 'entry_points': ['php_laminas'],
99
+ },
100
+ {
101
+ 'repo': 'attackmap-analyzer-omeka-s',
102
+ 'package': 'attackmap-analyzer-omeka-s',
103
+ 'version': '0.2.0',
104
+ 'git_sha': '9759c163e30c35256558188cff30dc1cf1331cd5',
105
+ 'entry_points': ['omeka_s'],
106
+ },
107
+ {
108
+ 'repo': 'attackmap-analyzer-iac',
109
+ 'package': 'attackmap-analyzer-iac',
110
+ 'version': '0.2.0',
111
+ 'git_sha': 'ef7811cb500b40535740fc104a3d2b29ef627141',
112
+ 'entry_points': ['iac'],
113
+ },
114
+ ]
@@ -0,0 +1,193 @@
1
+ """Bundled popular-package names for offline typosquat detection (#247).
2
+
3
+ Versioned, offline and deterministic: no network lookup ever happens. The
4
+ lists are curated from widely used packages (PyPI download rankings and the
5
+ most-depended-upon npm packages), and also carry near-twins of popular names
6
+ (`boto`/`boto3`, `scipy`/`scapy`) so a legitimate package isn't flagged as a
7
+ squat of its neighbour. Bump ``POPULAR_PACKAGES_VERSION`` when editing.
8
+
9
+ PyPI names are PEP 503-normalized (lowercase, runs of ``-_.`` → ``-``).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ POPULAR_PACKAGES_VERSION = "2026.10.1"
15
+
16
+ PYPI: frozenset[str] = frozenset((
17
+ "accelerate", "adal", "aiobotocore", "aiohttp", "aiosignal", "alabaster",
18
+ "alembic", "altair", "amqp", "angr", "annotated-types", "ansible", "ansible-core",
19
+ "anthropic", "anyio", "apscheduler", "arcade", "argon2-cffi", "ariadne", "arrow",
20
+ "asgiref", "astroid", "asttokens", "async-timeout", "asyncpg", "attrs", "authlib",
21
+ "automat", "autopep8", "awscli", "azure-common", "azure-core", "azure-identity",
22
+ "azure-storage-blob", "babel", "backoff", "bandit", "bcrypt", "beautifulsoup4",
23
+ "bidict", "billiard", "binaryornot", "black", "bleach", "blinker", "bokeh", "boto",
24
+ "boto3", "botocore", "build", "cachecontrol", "cachetools", "capstone",
25
+ "cassandra-driver", "catboost", "cattrs", "celery", "certifi", "cffi", "cfgv",
26
+ "channels", "chardet", "charset-normalizer", "click", "cloudpickle", "cmake",
27
+ "colorama", "colorlog", "comm", "comtypes", "confluent-kafka", "constantly",
28
+ "contourpy", "cookiecutter", "coverage", "croniter", "cryptography", "cssselect",
29
+ "cx-freeze", "cycler", "cython", "cytoolz", "daphne", "dash", "dask",
30
+ "dataclasses-json", "datadog", "datasets", "ddtrace", "debugpy", "decorator",
31
+ "deepdiff", "defusedxml", "dill", "diskcache", "distlib", "distributed", "django",
32
+ "django-cors-headers", "djangorestframework", "dnspython", "docker", "docutils",
33
+ "duckdb", "dynaconf", "ecdsa", "elasticsearch", "email-validator", "emoji",
34
+ "environs", "et-xmlfile", "eventlet", "exceptiongroup", "execnet", "executing",
35
+ "fabric", "factory-boy", "faker", "fastapi", "feedparser", "filelock", "findspark",
36
+ "fiona", "flake8", "flask", "flask-cors", "flask-jwt-extended", "flask-login",
37
+ "flask-migrate", "flask-restful", "flask-sqlalchemy", "flask-wtf", "flit-core",
38
+ "fonttools", "freezegun", "frida", "frida-tools", "frozenlist", "fsspec", "ftfy",
39
+ "gensim", "geopandas", "gevent", "gnupg", "google-api-core",
40
+ "google-api-python-client", "google-auth", "google-auth-httplib2",
41
+ "google-auth-oauthlib", "google-cloud-core", "google-cloud-storage",
42
+ "google-crc32c", "google-resumable-media", "googleapis-common-protos", "gradio",
43
+ "graphene", "graphql-core", "greenlet", "grpcio", "grpcio-status", "gunicorn",
44
+ "h11", "h5py", "hatchling", "html5lib", "httpcore", "httplib2", "httptools",
45
+ "httpx", "huggingface-hub", "humanize", "hyperlink", "hypothesis", "identify",
46
+ "idna", "imageio", "imagesize", "impacket", "importlib-metadata", "incremental",
47
+ "iniconfig", "invoke", "ipaddress", "ipykernel", "ipython", "ipywidgets",
48
+ "isodate", "isort", "itemadapter", "itsdangerous", "jaraco-classes", "jedi",
49
+ "jeepney", "jinja2", "jinja2-time", "jmespath", "joblib", "jsonpatch",
50
+ "jsonpath-ng", "jsonpointer", "jsonschema", "jsonschema-specifications", "jupyter",
51
+ "jupyter-client", "jupyter-core", "jupyterlab", "jupyterlab-server", "jwcrypto",
52
+ "kafka-python", "keras", "keyring", "keystone-engine", "kivy", "kiwisolver",
53
+ "kombu", "kubernetes", "langchain", "langchain-community", "langchain-core",
54
+ "langdetect", "lief", "lightgbm", "llvmlite", "locket", "loguru", "lxml", "mako",
55
+ "markdown", "markdown-it-py", "markupsafe", "marshmallow", "matplotlib",
56
+ "matplotlib-inline", "mccabe", "mdurl", "meson", "mistune", "mkdocs",
57
+ "mkdocs-material", "mock", "more-itertools", "moto", "motor", "mpmath", "msal",
58
+ "msal-extensions", "msgpack", "msrest", "msrestazure", "multidict", "mypy",
59
+ "mypy-extensions", "mysqlclient", "nbclient", "nbconvert", "nbformat",
60
+ "nest-asyncio", "netaddr", "netcdf4", "netifaces", "networkx", "ninja", "nltk",
61
+ "nmap", "nodeenv", "nose", "nose2", "notebook", "nox", "numba", "numpy",
62
+ "oauth2client", "oauthlib", "openai", "opencv-python", "opencv-python-headless",
63
+ "openpyxl", "opentelemetry-api", "opentelemetry-sdk", "orjson", "packaging",
64
+ "pandas", "pandocfilters", "paramiko", "parsel", "parso", "partd", "passlib",
65
+ "patsy", "pdm", "peewee", "pefile", "pendulum", "pexpect", "phonenumbers", "pika",
66
+ "pillow", "pip", "pkginfo", "platformdirs", "playwright", "plotly", "pluggy",
67
+ "poetry", "poetry-core", "polars", "pony", "pre-commit", "prettytable",
68
+ "prometheus-client", "prompt-toolkit", "protobuf", "psutil", "psycopg", "psycopg2",
69
+ "psycopg2-binary", "ptyprocess", "pure-eval", "pwntools", "py2exe", "py4j",
70
+ "pyarrow", "pyasn1", "pyasn1-modules", "pybind11", "pycodestyle", "pycountry",
71
+ "pycparser", "pycryptodome", "pycryptodomex", "pydantic", "pydantic-core",
72
+ "pydantic-settings", "pyelftools", "pyflakes", "pygame", "pyglet", "pygments",
73
+ "pyhamcrest", "pyinstaller", "pyjwt", "pylint", "pymdown-extensions", "pymongo",
74
+ "pymysql", "pynacl", "pyobjc", "pyopengl", "pyopenssl", "pyotp", "pyparsing",
75
+ "pyppeteer", "pyproj", "pyproject-hooks", "pyqt5", "pyqt6", "pyquery",
76
+ "pyrsistent", "pyside6", "pysnmp", "pyspark", "pytest", "pytest-asyncio",
77
+ "pytest-cov", "pytest-mock", "pytest-timeout", "pytest-xdist", "python-dateutil",
78
+ "python-dotenv", "python-engineio", "python-gnupg", "python-jose",
79
+ "python-multipart", "python-nmap", "python-slugify", "python-socketio", "pytz",
80
+ "pyvmomi", "pywin32", "pywin32-ctypes", "pyyaml", "pyzmq", "qrcode", "r2pipe",
81
+ "readme-renderer", "redis", "redis-py-cluster", "referencing", "regex", "requests",
82
+ "requests-mock", "requests-oauthlib", "responses", "retrying", "rich", "ropgadget",
83
+ "rpds-py", "rsa", "ruff", "s3fs", "s3transfer", "safetensors", "salt", "scapy",
84
+ "schedule", "scikit-build", "scikit-image", "scikit-learn", "scipy", "scrapy",
85
+ "seaborn", "secretstorage", "selenium", "sentencepiece", "sentry-sdk",
86
+ "service-identity", "setuptools", "setuptools-scm", "shapely", "shellingham",
87
+ "shodan", "simplejson", "six", "sniffio", "snowballstemmer",
88
+ "snowflake-connector-python", "sortedcontainers", "soupsieve", "spacy", "sphinx",
89
+ "sphinx-rtd-theme", "sqlalchemy", "sqlalchemy-utils", "sqlparse", "stack-data",
90
+ "starlette", "statsmodels", "strawberry-graphql", "streamlit", "structlog",
91
+ "sympy", "tables", "tabulate", "tblib", "tenacity", "tensorboard", "tensorflow",
92
+ "termcolor", "text-unidecode", "threadpoolctl", "tifffile", "tiktoken", "tinycss2",
93
+ "tokenizers", "toml", "tomli", "tomlkit", "toolz", "torch", "torchaudio",
94
+ "torchvision", "tornado", "tortoise-orm", "tox", "tqdm", "traitlets",
95
+ "transformers", "twine", "twisted", "typer", "typing-extensions", "tzdata",
96
+ "ujson", "unicorn", "unidecode", "uritemplate", "urllib3", "uvicorn", "uvloop",
97
+ "vine", "virtualenv", "w3lib", "watchdog", "watchfiles", "wcwidth", "webencodings",
98
+ "websocket-client", "websockets", "werkzeug", "wheel", "wrapt", "wtforms",
99
+ "wxpython", "xarray", "xgboost", "xmltodict", "yapf", "yara-python", "yarl",
100
+ "z3-solver", "zict", "zipp", "zope-interface",
101
+ ))
102
+
103
+ NPM: frozenset[str] = frozenset((
104
+ "@apollo/client", "@aws-sdk/client-s3", "@babel/core", "@babel/generator",
105
+ "@babel/parser", "@babel/preset-env", "@babel/preset-react",
106
+ "@babel/preset-typescript", "@babel/runtime", "@babel/traverse", "@babel/types",
107
+ "@emotion/react", "@emotion/styled", "@material-ui/core", "@mui/material",
108
+ "@nestjs/common", "@nestjs/core", "@prisma/client", "@reduxjs/toolkit",
109
+ "@tanstack/react-query", "@types/express", "@types/jest", "@types/lodash",
110
+ "@types/node", "@types/react", "@types/react-dom",
111
+ "@typescript-eslint/eslint-plugin", "@typescript-eslint/parser", "accepts",
112
+ "adm-zip", "ajv", "ajv-keywords", "amqplib", "angular", "ansi-regex",
113
+ "ansi-styles", "antd", "anymatch", "apollo-server", "archiver", "array-flatten",
114
+ "async", "asynckit", "autoprefixer", "aws-sdk", "axios", "babel-core",
115
+ "babel-loader", "balanced-match", "base64-js", "bcrypt", "bcryptjs",
116
+ "better-sqlite3", "big.js", "binary-extensions", "bindings", "bl", "bluebird",
117
+ "body-parser", "bootstrap", "boxen", "brace-expansion", "braces", "browserify",
118
+ "browserslist", "buffer", "buffer-from", "bull", "bullmq", "bunyan", "busboy",
119
+ "bytes", "call-bind", "callsites", "camelcase", "caniuse-lite", "canvas", "chai",
120
+ "chalk", "chart.js", "cheerio", "chokidar", "chownr", "class-transformer",
121
+ "class-validator", "classnames", "clean-css", "cli-spinners", "clipboardy",
122
+ "cliui", "clone", "color-convert", "color-name", "colors", "combined-stream",
123
+ "commander", "compression", "concat-map", "concurrently", "configstore",
124
+ "content-disposition", "content-type", "cookie", "cookie-parser",
125
+ "copy-webpack-plugin", "core-js", "core-util-is", "cors", "cosmiconfig", "cpy-cli",
126
+ "cross-env", "cross-fetch", "cross-spawn", "crypto-js", "css-loader", "cssnano",
127
+ "csv-parse", "d3", "date-fns", "dayjs", "debug", "decamelize", "deep-equal",
128
+ "deep-extend", "deepmerge", "define-properties", "delayed-stream", "depd",
129
+ "destroy", "dotenv", "duplexify", "ee-first", "ejs", "electron-to-chromium",
130
+ "emojis-list", "encodeurl", "end-of-stream", "engine.io", "enhanced-resolve",
131
+ "enquirer", "es-abstract", "esbuild", "escape-html", "escape-string-regexp",
132
+ "eslint", "eslint-config-airbnb", "eslint-config-prettier", "eslint-plugin-import",
133
+ "eslint-plugin-jsx-a11y", "eslint-plugin-prettier", "eslint-plugin-react",
134
+ "eslint-plugin-react-hooks", "etag", "eventemitter3", "events", "exceljs", "execa",
135
+ "express", "express-session", "fast-deep-equal", "fast-json-stable-stringify",
136
+ "fast-xml-parser", "fastify", "figures", "file-loader", "fill-range",
137
+ "finalhandler", "find-up", "firebase", "firebase-admin",
138
+ "fork-ts-checker-webpack-plugin", "form-data", "formidable", "formik", "forwarded",
139
+ "fresh", "fs-extra", "fs-minipass", "fsevents", "function-bind", "get-caller-file",
140
+ "get-intrinsic", "get-stream", "glob", "glob-parent", "googleapis", "got",
141
+ "graceful-fs", "graphql", "graphql-tag", "grunt", "gulp", "handlebars", "hapi",
142
+ "has", "has-flag", "hasown", "helmet", "highlight.js", "history",
143
+ "html-webpack-plugin", "http-errors", "http-proxy", "http-proxy-middleware",
144
+ "human-signals", "husky", "iconv-lite", "ieee754", "immer", "immutable",
145
+ "import-fresh", "inflight", "inherits", "ini", "inquirer", "ioredis", "ipaddr.js",
146
+ "is-core-module", "is-docker", "is-extglob", "is-glob", "is-number", "is-stream",
147
+ "is-wsl", "isarray", "isexe", "isomorphic-fetch", "jasmine", "jest", "jimp", "joi",
148
+ "jquery", "js-tokens", "js-yaml", "jsdom", "json-schema-traverse",
149
+ "json-stable-stringify", "json5", "jsonfile", "jsonwebtoken", "jszip", "kafkajs",
150
+ "karma", "kleur", "knex", "koa", "koa-router", "latest-version", "leaflet", "less",
151
+ "lint-staged", "listr2", "loader-utils", "locate-path", "lodash",
152
+ "lodash.debounce", "lodash.get", "lodash.isequal", "lodash.merge", "log-symbols",
153
+ "log4js", "loose-envify", "lru-cache", "luxon", "mapbox-gl", "markdown-it",
154
+ "marked", "media-typer", "memory-fs", "meow", "merge-descriptors", "merge-stream",
155
+ "methods", "micromatch", "mime", "mime-db", "mime-types", "mimic-fn",
156
+ "mini-css-extract-plugin", "minimatch", "minimist", "minipass", "mitt", "mkdirp",
157
+ "mobx", "mocha", "moment", "mongodb", "mongoose", "morgan", "ms", "multer",
158
+ "mustache", "mysql", "mysql2", "nan", "nanoid", "needle", "negotiator",
159
+ "neo-async", "next", "node-addon-api", "node-fetch", "node-gyp", "node-releases",
160
+ "nodemailer", "nodemon", "normalize-path", "npm-run-all", "npm-run-path",
161
+ "numeral", "nunjucks", "nuxt", "object-assign", "object-keys", "on-finished",
162
+ "once", "onetime", "open", "opn", "ora", "p-limit", "p-locate", "p-try",
163
+ "package-json", "papaparse", "parcel", "parent-module", "passport", "passport-jwt",
164
+ "passport-local", "path-exists", "path-is-absolute", "path-key", "path-parse",
165
+ "path-to-regexp", "pdfkit", "pg", "pg-promise", "picocolors", "picomatch", "pino",
166
+ "pkg-dir", "playwright", "postcss", "postcss-loader", "prebuild-install",
167
+ "prettier", "prisma", "process-nextick-args", "prompts", "prop-types",
168
+ "proxy-addr", "psl", "pug", "pump", "punycode", "puppeteer", "qs", "query-string",
169
+ "querystring", "range-parser", "raw-body", "rc", "react", "react-bootstrap",
170
+ "react-dom", "react-hook-form", "react-query", "react-redux", "react-router",
171
+ "react-router-dom", "readable-stream", "readdirp", "recoil", "redis", "redux",
172
+ "redux-saga", "redux-thunk", "reflect-metadata", "regenerator-runtime",
173
+ "registry-url", "request", "require-directory", "resolve", "resolve-from",
174
+ "restify", "rimraf", "rollup", "rxjs", "rxjs-compat", "safe-buffer", "sass",
175
+ "sass-loader", "schema-utils", "selenium-webdriver", "semver", "send", "sequelize",
176
+ "serve-favicon", "serve-static", "setprototypeof", "sharp", "shebang-command",
177
+ "shebang-regex", "shelljs", "showdown", "signal-exit", "sinon", "socket.io",
178
+ "socket.io-client", "source-map", "source-map-support", "sqlite3", "statuses",
179
+ "stream-shift", "string-width", "string_decoder", "strip-ansi",
180
+ "strip-final-newline", "strip-json-comments", "stripe", "style-loader",
181
+ "styled-components", "stylus", "superagent", "supports-color", "svelte", "swr",
182
+ "tailwindcss", "tapable", "tar", "tar-stream", "terser", "terser-webpack-plugin",
183
+ "three", "through2", "to-regex-range", "toidentifier", "tough-cookie", "ts-loader",
184
+ "ts-node", "tslib", "twilio", "type-is", "typeorm", "typescript", "uglify-js",
185
+ "underscore", "universalify", "unpipe", "unzipper", "update-browserslist-db",
186
+ "update-notifier", "uri-js", "url-loader", "url-parse", "util-deprecate",
187
+ "utils-merge", "uuid", "validator", "vary", "vite", "vue", "watchpack", "webpack",
188
+ "webpack-cli", "webpack-dev-server", "whatwg-fetch", "which", "winston",
189
+ "wrap-ansi", "wrappy", "ws", "xdg-basedir", "xlsx", "xml2js", "y18n", "yallist",
190
+ "yaml", "yargs", "yargs-parser", "yup", "zod", "zone.js", "zustand",
191
+ ))
192
+
193
+ BY_ECOSYSTEM: dict[str, frozenset[str]] = {"pypi": PYPI, "npm": NPM}
attackmap/progress.py ADDED
@@ -0,0 +1,258 @@
1
+ """Live scan progress with ETA (#36), for terminals and for front-ends.
2
+
3
+ A repo scan can run for many minutes on a large tree, and a silent terminal
4
+ reads as "hung." This renders a single, self-updating status line to stderr:
5
+ a bar + percentage + count + ETA during the per-file pass, and an animated
6
+ spinner with elapsed time during the indeterminate tail analyzers (taint,
7
+ SBOM, authorization, anomalies) — the phase where a big monorepo spends most
8
+ of its time.
9
+
10
+ Zero third-party deps. TTY-aware: when stderr isn't a terminal (CI, piped to
11
+ a file) it stays silent so logs aren't polluted with carriage returns. The
12
+ scanner drives it through the small hook surface (`begin`/`advance`/`stage`/
13
+ `done`); when no progress object is passed, the scan runs exactly as before.
14
+
15
+ Two renderers implement that hook surface: :class:`ScanProgress` (the TTY
16
+ status line above) and :class:`JsonScanProgress`, which always emits NDJSON
17
+ events for the macOS GUI and other non-TTY front-ends.
18
+ :func:`create_progress` picks one from ``--progress-format``.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import sys
25
+ import threading
26
+ import time
27
+ from typing import TextIO
28
+
29
+ # NDJSON progress protocol version (consumed by the macOS GUI and other
30
+ # non-TTY front-ends). Bump only on a breaking change to the event shape.
31
+ PROGRESS_PROTOCOL_VERSION = 1
32
+
33
+ _SPINNER = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
34
+ _BAR_WIDTH = 24
35
+ _MIN_REDRAW_INTERVAL = 0.08 # seconds — cap redraws so we don't thrash the TTY
36
+
37
+
38
+ def _fmt_duration(seconds: float) -> str:
39
+ seconds = int(max(0, seconds))
40
+ if seconds >= 3600:
41
+ return f"{seconds // 3600}h{(seconds % 3600) // 60:02d}m"
42
+ return f"{seconds // 60:02d}:{seconds % 60:02d}"
43
+
44
+
45
+ class ScanProgress:
46
+ """Render scan progress to a TTY. All methods are no-ops when disabled."""
47
+
48
+ def __init__(self, *, enabled: bool = True, stream: TextIO | None = None) -> None:
49
+ stream = stream if stream is not None else sys.stderr
50
+ self._stream = stream
51
+ self._enabled = enabled and hasattr(stream, "isatty") and stream.isatty()
52
+ self._total = 0
53
+ self._done = 0
54
+ self._start = 0.0
55
+ self._last_draw = 0.0
56
+ self._last_len = 0
57
+ self._label = ""
58
+ # Spinner thread state for indeterminate stages.
59
+ self._spin_stop: threading.Event | None = None
60
+ self._spin_thread: threading.Thread | None = None
61
+ self._lock = threading.Lock()
62
+
63
+ # --- per-file phase ---------------------------------------------------
64
+
65
+ def begin(self, total: int, label: str = "Scanning files") -> None:
66
+ if not self._enabled:
67
+ return
68
+ self._stop_spinner()
69
+ self._total = max(0, total)
70
+ self._done = 0
71
+ self._label = label
72
+ self._start = time.monotonic()
73
+ self._draw(force=True)
74
+
75
+ def advance(self, current: str = "") -> None:
76
+ if not self._enabled:
77
+ return
78
+ self._done += 1
79
+ self._draw(current=current)
80
+
81
+ # --- indeterminate tail phases ---------------------------------------
82
+
83
+ def stage(self, label: str) -> None:
84
+ """Switch to an indeterminate phase with an animated spinner."""
85
+ if not self._enabled:
86
+ return
87
+ self._stop_spinner()
88
+ self._label = label
89
+ stop = threading.Event()
90
+ self._spin_stop = stop
91
+ started = time.monotonic()
92
+
93
+ def _spin() -> None:
94
+ i = 0
95
+ while not stop.is_set():
96
+ frame = _SPINNER[i % len(_SPINNER)]
97
+ with self._lock:
98
+ self._write(f"{frame} {label}… ({_fmt_duration(time.monotonic() - started)})")
99
+ i += 1
100
+ stop.wait(0.12)
101
+
102
+ thread = threading.Thread(target=_spin, daemon=True)
103
+ self._spin_thread = thread
104
+ thread.start()
105
+
106
+ def done(self, summary: str = "") -> None:
107
+ if not self._enabled:
108
+ return
109
+ self._stop_spinner()
110
+ with self._lock:
111
+ self._clear_line()
112
+ if summary:
113
+ self._stream.write(summary + "\n")
114
+ self._stream.flush()
115
+
116
+ # --- internals --------------------------------------------------------
117
+
118
+ def _stop_spinner(self) -> None:
119
+ if self._spin_stop is not None:
120
+ self._spin_stop.set()
121
+ if self._spin_thread is not None:
122
+ self._spin_thread.join(timeout=0.5)
123
+ self._spin_stop = None
124
+ self._spin_thread = None
125
+
126
+ def _draw(self, *, current: str = "", force: bool = False) -> None:
127
+ now = time.monotonic()
128
+ if not force and (now - self._last_draw) < _MIN_REDRAW_INTERVAL and self._done < self._total:
129
+ return
130
+ self._last_draw = now
131
+ total = self._total or 1
132
+ frac = min(1.0, self._done / total)
133
+ filled = int(_BAR_WIDTH * frac)
134
+ bar = "█" * filled + "░" * (_BAR_WIDTH - filled)
135
+ elapsed = now - self._start
136
+ eta = (elapsed / frac - elapsed) if frac > 0 else 0.0
137
+ tail = f" {_truncate(current, 32)}" if current else ""
138
+ line = (
139
+ f"{self._label} [{bar}] {int(frac * 100):3d}% "
140
+ f"{self._done}/{self._total} ETA {_fmt_duration(eta)}{tail}"
141
+ )
142
+ with self._lock:
143
+ self._write(line)
144
+
145
+ def _write(self, text: str) -> None:
146
+ # Pad to erase any leftover from a previous longer line.
147
+ pad = max(0, self._last_len - len(text))
148
+ self._stream.write("\r" + text + " " * pad)
149
+ self._stream.flush()
150
+ self._last_len = len(text)
151
+
152
+ def _clear_line(self) -> None:
153
+ if self._last_len:
154
+ self._stream.write("\r" + " " * self._last_len + "\r")
155
+ self._last_len = 0
156
+
157
+
158
+ def _truncate(text: str, width: int) -> str:
159
+ text = text.replace("\\", "/")
160
+ if len(text) <= width:
161
+ return text
162
+ return "…" + text[-(width - 1):]
163
+
164
+
165
+ class JsonScanProgress:
166
+ """Emit scan progress as newline-delimited JSON (one object per line).
167
+
168
+ Same hook surface as ``ScanProgress`` (``begin``/``advance``/``stage``/
169
+ ``done``) so the scanner drives either interchangeably. Unlike the TTY
170
+ renderer this never gates on ``isatty`` — its whole purpose is to feed a
171
+ non-terminal consumer (the macOS GUI) reading the child process's stderr.
172
+
173
+ Event shapes (all carry ``"v": PROGRESS_PROTOCOL_VERSION``)::
174
+
175
+ {"v":1,"event":"begin","total":1240,"label":"Scanning files"}
176
+ {"v":1,"event":"advance","done":37,"total":1240,"current":"src/app.ts"}
177
+ {"v":1,"event":"stage","label":"Taint analysis"}
178
+ {"v":1,"event":"done","summary":"…","done":1240,"total":1240,"elapsed_s":92.4}
179
+
180
+ ``advance`` events are throttled to at most one per ``min_interval`` seconds
181
+ to keep the stream sane on large trees; ``done`` (per-file count) stays
182
+ accurate because every advance still increments the internal counter and the
183
+ latest count rides on each emitted event.
184
+ """
185
+
186
+ def __init__(self, *, stream: TextIO | None = None, min_interval: float = 0.1) -> None:
187
+ self._stream = stream if stream is not None else sys.stderr
188
+ self._min_interval = max(0.0, min_interval)
189
+ self._total = 0
190
+ self._done = 0
191
+ self._start = 0.0
192
+ self._last_emit = 0.0
193
+
194
+ def begin(self, total: int, label: str = "Scanning files") -> None:
195
+ self._total = max(0, total)
196
+ self._done = 0
197
+ self._start = time.monotonic()
198
+ self._last_emit = self._start # anchor throttle window to scan start
199
+ self._emit("begin", total=self._total, label=label)
200
+
201
+ def advance(self, current: str = "") -> None:
202
+ self._done += 1
203
+ now = time.monotonic()
204
+ # Always emit the final file; throttle the rest.
205
+ if self._done < self._total and (now - self._last_emit) < self._min_interval:
206
+ return
207
+ self._last_emit = now
208
+ self._emit("advance", done=self._done, total=self._total, current=current)
209
+
210
+ def stage(self, label: str) -> None:
211
+ self._emit("stage", label=label)
212
+
213
+ def done(self, summary: str = "") -> None:
214
+ elapsed = round(time.monotonic() - self._start, 3) if self._start else 0.0
215
+ self._emit(
216
+ "done",
217
+ summary=summary,
218
+ done=self._done,
219
+ total=self._total,
220
+ elapsed_s=elapsed,
221
+ )
222
+
223
+ def _emit(self, event: str, **fields: object) -> None:
224
+ payload = {"v": PROGRESS_PROTOCOL_VERSION, "event": event, **fields}
225
+ try:
226
+ self._stream.write(json.dumps(payload, ensure_ascii=False) + "\n")
227
+ self._stream.flush()
228
+ except (ValueError, OSError):
229
+ # Never let a broken/closed pipe take down a scan.
230
+ pass
231
+
232
+
233
+ def create_progress(
234
+ fmt: str = "auto",
235
+ *,
236
+ no_progress: bool = False,
237
+ stream: TextIO | None = None,
238
+ ):
239
+ """Build the right progress sink for ``--progress-format``.
240
+
241
+ - ``none`` (or ``no_progress=True``) → a disabled ``ScanProgress`` no-op.
242
+ - ``json`` → :class:`JsonScanProgress` (NDJSON, always emits).
243
+ - ``auto`` / ``tty`` → :class:`ScanProgress` (TTY bar, self-silences when
244
+ stderr isn't a terminal).
245
+ """
246
+ if no_progress or fmt == "none":
247
+ return ScanProgress(enabled=False, stream=stream)
248
+ if fmt == "json":
249
+ return JsonScanProgress(stream=stream)
250
+ return ScanProgress(enabled=True, stream=stream)
251
+
252
+
253
+ __all__ = [
254
+ "ScanProgress",
255
+ "JsonScanProgress",
256
+ "create_progress",
257
+ "PROGRESS_PROTOCOL_VERSION",
258
+ ]
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ # Phase-1 shared recon/result model exports.
4
+ # Keep ScanResult and recon signal models as the single source of truth via models.py.
5
+ from .models import (
6
+ AuthHint,
7
+ DatabaseHint,
8
+ EdgeHint,
9
+ EntrypointHint,
10
+ ExternalCall,
11
+ FrameworkHint,
12
+ ProtocolHint,
13
+ Route,
14
+ ScanResult,
15
+ SecretHint,
16
+ ServiceHint,
17
+ )
18
+
19
+ __all__ = [
20
+ "Route",
21
+ "ExternalCall",
22
+ "DatabaseHint",
23
+ "AuthHint",
24
+ "ServiceHint",
25
+ "EdgeHint",
26
+ "EntrypointHint",
27
+ "ProtocolHint",
28
+ "FrameworkHint",
29
+ "SecretHint",
30
+ "ScanResult",
31
+ ]