diff --git a/.coveragerc b/.coveragerc index 08b3ba08..e36f1176 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,6 +7,7 @@ omit = */docs/* */benchmarks/* */scripts/* + */examples/* [report] show_missing = true @@ -17,6 +18,7 @@ omit = */benchmarks/* */docs/* */scripts/* + */examples/* [html] directory = htmlcov diff --git a/.dockerignore b/.dockerignore index 7256dd1d..60e2cb96 100644 --- a/.dockerignore +++ b/.dockerignore @@ -93,4 +93,6 @@ debian/*.substvars # Misc TODO -ROADMAP.md \ No newline at end of file +ROADMAP.md +test.py +examples \ No newline at end of file diff --git a/.gitignore b/.gitignore index dbd18909..b748ae63 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,4 @@ debian/*.substvars *.dsc *.tar.xz /debug.sh +/test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c05248d8..90185351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ CHANGELOG ======= +v5.16.2 (31.05.2026) +--------------------------- +- (critical) fixed scan crashes caused by corrupted gzip/encoded HTTP responses by handling `DecodeError` as a recoverable transport failure instead of aborting worker threads. +- (fix) JavaScript cookie-gate bootstrap pages such as `document.cookie` + `location.reload()` responses are no longer reported as `OK` findings. +- (fix) subdomain scans so missing/no-response candidates are skipped without triggering the directory retry fail-streak abort guard. +- (fix) directory scan prefix normalization so `--prefix ex` and `--prefix ex/` both scan under `/ex/` instead of concatenating paths as `/ex`. +- (fix) reduced WAF-safe auto-calibration noise by using neutral calibration probe paths when `--waf-safe-mode` is enabled, avoiding high-risk `.php`, `.map`, `admin`, and `wp-*` probe shapes. +- (fix) `--fingerprint` no longer treats generic WordPress static-path probes as strong WordPress evidence unless corroborated by root-page WordPress signals. +- (fix) `--auto-calibrate` now disables weak HTTP baselines when too many probes are blocked, ignored, or failed, preventing sparse signatures from over-filtering scan results. +- (fix) `--sniff shadow` false positives on soft-200/fallback routes by adding a negative-control probe before reporting backup-file variants. +- (fix) `--sniff malware` false positives when fallback pages repeatedly echo webshell-like names inside URL/query attributes, while preserving real webshell UI and executable payload detections. +- (fix) `--sniff malware` false positives on security-plugin documentation by suppressing name-only webshell vocabulary in documentation context while preserving executable payload and shell UI detections. +- (fix) `--sniff malware` false positives on legacy Google Analytics loaders while preserving suspicious document.write, atob, String.fromCharCode and PHP payload detections. +- (fix) `--sniff secret` scan output so secret sniffer hits are labeled as `OK (Secret)` like other sniffer findings. +- (fix) `--fingerprint` now detects DataLife Engine (DLE) from conservative runtime globals and engine asset signals. +- (fix) `--fingerprint` now prefers Webflow hosted-platform signals over endpoint-only WordPress static path artifacts. +- (fix) `--fingerprint` now detects CMS.S3 / Megagroup from strong root-page builder/runtime markers without relying on generic WordPress endpoint probes. +- (fix) transport-exhausted directory entries are now tracked in `transport_failed.txt` and JSON diagnostics, and scans automatically pause after repeated transport failures to avoid burning through the wordlist during temporary network outages. +- (enhancement) added Camaleon CMS without adding active probes. +- (enhancement) added Evolution CMS fingerprint detection. +- (enhancement) added strong UMI.CMS fingerprint detection rules. +- (enhancement) added Melbis Shop Platform fingerprint detection rules. +- (enhancement) added conservative MogutaCMS fingerprint detection without active probes. +- (enhancement) added Ruby on Rails fingerprint detection with conservative passive CSRF, Rails UJS/Turbo, asset-pipeline and Rails error markers while avoiding standalone Rack. +- (enhancement) reduced `--sniff malware` false positives for standard Bitrix admin login pages by allowlisting the built-in hidden `auth_frame` iframe only when strong Bitrix login markers are present. +- (enhancement) `--sniff secret` now detects additional low-noise token patterns, including GitHub fine-grained tokens, Square-style tokens, leaked bearer headers and expanded credential assignments. +- (ui) clarified Runtime Diagnostics queue accounting by showing consumed items, submitted HTTP jobs, and pre-request skipped items separately. +- (ui) clarified runtime pause/resume behavior by making the Ctrl+C pause prompt visible after in-flight worker output drains and by documenting Enter/C continue and E/Q abort semantics. +- (dictionary) cleaned and normalized the internal directories list (+1247 potential interesting paths). +- (docs) added a `Mastering OpenDoor` companion documentation page for the upcoming article series. +- (deps-dev) [PR#115](https://github.com/stanislav-web/OpenDoor/pull/115) bump ruff from 0.15.13 to 0.15.14 in the python-runtime-dependencies group. + v5.16.1 (24.05.2026) --------------------------- - (fix) reduced duplicate fingerprint traffic by reusing exact same method+URL probe responses within a single fingerprint pass. @@ -78,7 +110,7 @@ v5.16.0 (17.05.2026) - (ux) reduced stdout Summary noise by hiding low-value diagnostic counters and detailed fingerprint/HSTS/privacy internals while preserving them in structured reports. - (ux) improved connection preflight diagnostics for localhost/proxy transport checks. - (dictionary) bundled `data/shadow-suffixes.dat` in source and wheel distributions so PyPI, Homebrew-style source builds and local installs include the built-in shadow suffix catalog by default. -- (dictionary) cleaned and normalized internal directories list (+2133 potencial interesting paths). +- (dictionary) cleaned and normalized internal directories list (+xxx potencial interesting paths). - (build) added staged Ruff quality gates and advisory Vulture dead-code checks, with updated contributor rules and cleanup documentation. v5.15.3 (09.05.2026) @@ -395,7 +427,7 @@ v5.10.0 (28.04.2026) - (dictionary) cleaned and normalized directories list - (dictionary) refreshed subdomains wordlist with `+1251780` entries - (tests) added unittest coverage for CI/CD fail-on exit codes -- (tests) added unittest coverage for adaptive cooldown behaviour +- (tests) added unittest coverage for adaptive cooldown behavior v5.9.2 (27.04.2026) --------------------------- diff --git a/README.md b/README.md index de7e7aa9..f88e8783 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ Useful sniffers include: | `skipsizes=46:1024` | Skip responses inside a noisy size range. | | `stacktrace` | Detect exposed debug/runtime stack traces and internal error details. | | `secret` | Detect possible exposed API keys, tokens, private keys and credentials with redacted report metadata. | -| `shadow` | Actively probe confirmed `200 OK` file-like hits for bounded backup/shadow variants such as `.bak`, `.old`, and path templates like `index2.php`. | +| `shadow` | Actively probe confirmed `200 OK` file-like hits for bounded backup/shadow variants such as `.bak`, `.old`, and path templates . | | `openredirect` | Actively verify redirect-like query parameters with controlled marker URLs and report only confirmed open redirect vulnerabilities. | | `malware` | Detect possible malicious content, webshell markers, injected scripts or obfuscated payloads. | diff --git a/VERSION b/VERSION index 272b6f7b..89f2faf2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.16.1 \ No newline at end of file +5.16.2 \ No newline at end of file diff --git a/data/directories.dat b/data/directories.dat index 2c145290..bcfb1bba 100644 --- a/data/directories.dat +++ b/data/directories.dat @@ -214,6 +214,7 @@ 1c-bitrix 1c-bitrix/admin.php 1c_exchange.php +1cadmin 1checkout.aspx 1cManager 1confirmssr.htm @@ -1924,6 +1925,7 @@ 403.php 403.shtml 403_manage.phtml +403bypass 403error.htm 403error.html 403error.php @@ -3808,7 +3810,6 @@ 7214.html 7215.html 7218.html -7243A_ODU_UDP_DL_OTA_Aisspan_eNB_R1.0.6.log 7279.php 7280.php 7288.php @@ -4779,9 +4780,6 @@ %20.. %2500.cfm %C0%AE%C0%AE%C0%AF -(compaq) -(dtcc) -(pty) ****** +CSCOE+/logon.html +CSCOE+/session_password.html @@ -4823,6 +4821,7 @@ ../../../../../../etc/passwd ../../../../../../home/ubuntu/.env ../../data/config/microsrv.cfg +..; ..;/examples/jsp/index.html ..;/examples/servlets/index.html ..;/examples/websocket/index.xhtml @@ -4848,6 +4847,7 @@ .addressbook .adm .admin +.admin.php.swp .adminer.php.swp .Administration .administration @@ -5186,6 +5186,7 @@ .db .db3 .db.env +.db.php.swp .db.xml .db.yaml .dbeaver/credentials-config.json @@ -5764,6 +5765,7 @@ .htaccess.orig .htaccess.sample .htaccess.save +.htaccess.swp .htaccess.txt .htaccess_extra .htaccess_orig @@ -5842,11 +5844,6 @@ .idea/webServers.xml .idea/woaWordpress.iml .idea/workspace(2).xml -.idea/workspace(3).xml -.idea/workspace(4).xml -.idea/workspace(5).xml -.idea/workspace(6).xml -.idea/workspace(7).xml .idea/workspace.xml .idea_modules .ideaassetwizardsettings.xml @@ -6009,6 +6006,7 @@ .log.txt .logfile .login +.login.php.swp .login_conf .logout .logs @@ -6101,6 +6099,7 @@ .nbgrader.log .nbproject .neomail +.nessus .net .netrc .netrwhist @@ -6266,6 +6265,7 @@ .pmd .pmtignore .png +.pnpm-store .pocketpc .poggit.yml .pomerium @@ -6776,6 +6776,7 @@ .well-known/ashrae .well-known/assetlinks.json .well-known/autoconfig/mail +.well-known/bimi .well-known/browserid .well-known/caldav .well-known/carddav @@ -6790,6 +6791,7 @@ .well-known/enterprise-transport-security .well-known/envoy .well-known/est +.well-known/est/cacerts .well-known/genid .well-known/hoba .well-known/host-meta @@ -6813,12 +6815,14 @@ .well-known/ni .well-known/nodeinfo .well-known/oauth-authorization-server +.well-known/oauth-protected-resource .well-known/okta-organization .well-known/openai-plugin.json .well-known/openid-configuration .well-known/openid-federation .well-known/openorg .well-known/openpgpkey +.well-known/passkey-endpoints .well-known/pki-validation .well-known/pomerium .well-known/pomerium/jwks.json @@ -6827,6 +6831,7 @@ .well-known/reload-config .well-known/repute-template .well-known/resourcesync +.well-known/robots.txt .well-known/security.txt .well-known/stun-key .well-known/thread @@ -6835,7 +6840,9 @@ .well-known/traffic-advice .well-known/uma2-configuration .well-known/void +.well-known/webauthn .well-known/webfinger +.well-known/well-known .well-knownacme-challenge .well-knownapple-app-site-association .well-knownapple-developer-merchant-domain-association @@ -7046,11 +7053,13 @@ __adminer.php __backup __bx_log.log __cache +__cf_chl_jschl_tk__ __clockwork/app __clockwork/latest __createdb.php __data __debug__ +__docs__ __documents __dummy.html __errfiles__ @@ -7079,6 +7088,7 @@ __php_errors.log __pma___ __pycache__ __recovery +__redoc__ __SQL __swagger__ __temp__ @@ -7246,6 +7256,8 @@ _beta _bin _bkup _blank.asp +_blazor +_blazor/negotiate _blog _blulab _bo @@ -7257,6 +7269,7 @@ _bugs.txt _build _buildManifest.js _bulk +_buran/seoModule.php _buy.html _c _cache @@ -7414,6 +7427,7 @@ _disc _disc1 _disc2 _dist +_dmarc.txt _doc _Dockerfile _docs @@ -7667,6 +7681,9 @@ _mapping _mappings _master _masters +_matrix/client/r0/login +_matrix/client/r0/register +_matrix/federation/v1/version _media _medienid _mem_bin @@ -8184,6 +8201,7 @@ a-004.htm a-005.htm a-006.htm a-007.htm +a-b a-blog a-propos a-propos.php @@ -8265,6 +8283,7 @@ aaapremier aaasc aaasocalifornia aaatexas +aad aadm.html aadmin aadmin.php @@ -8301,6 +8320,7 @@ aatest aau ab ab-test +ab-test/results ab.framework ab/docs ab_test @@ -8462,6 +8482,8 @@ aboutus.htm aboutus.html aboutus.php aboutus.shtml +ABP +abp/api abpost.php abraham abrametas-prod.php @@ -9403,6 +9425,7 @@ aclogic acls acm acme +acme-challenge acme-challenge/.env acme/.env acme_challenges/.env @@ -9475,6 +9498,7 @@ action.filesform.php action.html action.importxml.php action.jsp +action.log action.module.php action.newdir.php action.php @@ -9487,8 +9511,10 @@ action.topic.php action.transfer.php action.upload.php action.validate.php +action_cable action_custom.php action_emty.php +action_mailer actionalert.asp actionapps actionfile @@ -9556,12 +9582,15 @@ active.asp active.html active.log active.php +active_admin +active_admin/dashboard active_polls.asp active_port_get.cfm active_power_out.php active_topics.asp active_total_power.php active_users.asp +activeadmin activecalendar.php activecampaign activeCollab @@ -9614,6 +9643,7 @@ activity.php activity_char.php activity_favs.php activitynames.php +activitypub activitysessions/docs activkey Activpp @@ -9691,6 +9721,7 @@ actuator/caches actuator/conditions actuator/configprops actuator/configurationMetadata +actuator/druid actuator/dump actuator/env actuator/events @@ -9726,6 +9757,7 @@ actuator/shutdown actuator/springWebflow actuator/sso actuator/ssoSessions +actuator/startup actuator/statistics actuator/status actuator/threaddump @@ -10549,6 +10581,8 @@ adm/authorization.php adm/authorize.php adm/backend.php adm/backoffice.php +adm/callback.log +adm/catch.php adm/check-login.php adm/check.php adm/check_login.php @@ -10573,6 +10607,7 @@ adm/dashboard.phtml adm/default.php adm/entry.php adm/fckeditor +adm/git.txt adm/home.php adm/index adm/index.asp @@ -10904,7 +10939,12 @@ admin-area-settings admin-area-support admin-area-tools admin-area-users +admin-area-v2/auth +admin-area-v2/login +admin-area-v2/signin +admin-area.htm admin-area.php +admin-area.phtml admin-area/account admin-area/account.php admin-area/accounts.php @@ -11680,6 +11720,10 @@ admin-users admin-web admin-web.php admin-wjg +admin-zone +admin-zone/auth +admin-zone/login +admin-zone/signin admin. admin../admin admin.admin.html.php @@ -11758,6 +11802,8 @@ ADMIN.php Admin.php admin.php3 admin.php.back +admin.php.orig +admin.php.swp admin.phpadmin.html admin.phtml Admin.pl @@ -12550,6 +12596,7 @@ admin/oauth.php admin/old admin/Oledit admin/operator.php +admin/order_tracking.php admin/orders admin/organizations admin/outgoing @@ -12610,6 +12657,8 @@ admin/PMA/index.php admin/PMAindex.php admin/pmaindex.php admin/pol_log.txt +admin/polls/resultwin.php +admin/polls/resultwin.php3 admin/portal admin/portal.php admin/portalcollect.php @@ -12683,10 +12732,12 @@ admin/roles.php admin/root admin/root.php admin/routes +admin/routes.txt admin/rules admin/runtime/tree admin/saml admin/save.php +admin/scale.php admin/scheduler admin/schema admin/scripts/fckeditor @@ -12853,6 +12904,7 @@ admin/uhome.html admin/up admin/update.php admin/upfile.asp +admin/upgrade.php admin/upload admin/upload.asp admin/upload.php @@ -13733,6 +13785,7 @@ admin_iprev.html admin_iprev.php admin_js admin_js.php +admin_ka_wx.php admin_ldown.asp admin_ldown.html admin_ldown.php @@ -16599,6 +16652,7 @@ administrator/login.cgi administrator/login.html Administrator/login.html administrator/login.js +administrator/login.log administrator/login.php Administrator/login.php administrator/login.phtml @@ -16617,7 +16671,9 @@ administrator/maintenance administrator/manage.php administrator/manager.php administrator/manifests +administrator/manifests/files/joomla.xml administrator/manifests/filesjoomla.xml +administrator/manifests/packages administrator/media administrator/member administrator/member.php @@ -17745,7 +17801,11 @@ adminxxx.php adminz adminz.php adminzone +adminzone-new +adminzone-old adminzone.php +adminzone/auth +adminzone_old admin~ admiral admisapi @@ -18207,6 +18267,8 @@ aenovoshop aeon aep aerepair.aspx +aerfans +aermember.php aero aerobics aeromail @@ -18262,6 +18324,7 @@ affiliate.js affiliate.php Affiliate.php affiliate/login +affiliate/track affiliate_admin affiliate_admin.php affiliate_area.php @@ -18879,6 +18942,7 @@ ainfo.php ainstall ainstall.json ainstall.php +aiohttp aiops aiops/admin.php aiops/config.php @@ -18914,6 +18978,7 @@ airaksinen airasiago airbnb airbnb-get.php +airbrake airconditioner1.php airconditioner2.php airconditioner3.php @@ -19001,8 +19066,10 @@ ajax/adm ajax/adm.php ajax/admin ajax/admin.php +ajax/ajax.js ajax/api ajax/data +ajax/search.php ajax_ ajax_action.php ajax_bookmarks.php @@ -19119,6 +19186,7 @@ ajuiza_grava_f.php ajuiza_pg_00_f.php ajx ajxaction +ajxp.php ak ak47.php ak47.phtml @@ -19127,6 +19195,7 @@ ak74shell.php AK-74.php ak-systems akamai +akamai/purge akarru akatsuki akce @@ -19287,6 +19356,9 @@ aleatorio.php alegria aleja alemania +alembic +alembic/env.py +alembic/versions alentum alert alert.asp @@ -19775,6 +19847,8 @@ amp amp.php ampache amphor@ +amplitude/2/httpapi +amplitude/httpapi amqp amrefresh.asp amrhein @@ -19875,28 +19949,39 @@ analytics.html analytics.php analytics.txt analytics/admin +analytics/alias +analytics/api analytics/api.php +analytics/batch analytics/chart.php analytics/check.php +analytics/collect analytics/csv.php analytics/dashboard analytics/dashboard.php analytics/data.php +analytics/event +analytics/events analytics/graph.php +analytics/group analytics/health.php +analytics/identify analytics/index.php analytics/json.php analytics/login analytics/metrics.php analytics/monitor.php +analytics/page analytics/panel.php analytics/probe.php analytics/report.php analytics/saw.dll?bieehome&startPage=1#grabautologincookies analytics/saw.dll?getPreviewImage&previewFilePath=/etc/passwd analytics/saw.dll?getPreviewImage&previewFilePath=/etcpasswd +analytics/screen analytics/stats.php analytics/status.php +analytics/track analytics/view.php analytics/xml.php analyticstracking.php @@ -20414,8 +20499,10 @@ api-gateway/.env.dev api-gateway/.env.local api-gateway/.env.production api-gateway/.env.staging +api-key api-key.json api-key.php +api-keys api-keys.txt api-reference api-v12.php @@ -20435,7 +20522,10 @@ api.py api.tar.gz api.tgz api.txt +api.wsdl api.xml +api.yaml +api.yml api.zip api/2/explore api/2/issue/createmeta @@ -20464,6 +20554,7 @@ api/.env_1.bak api/__swagger__ api/_preferences api/_swagger_ +api/abp/application-configuration api/access api/access.log api/Account/Login @@ -20584,6 +20675,8 @@ api/auth api/auth.config api/auth.php api/auth/csrf +api/auth/local +api/auth/local/register api/auth/login api/auth/providers api/auth/session @@ -20792,6 +20885,7 @@ api/cluster/spec api/cluster_conf api/collections api/collections.list +api/collections/users/records api/command api/command/list api/command/ping @@ -20866,10 +20960,12 @@ api/device/report api/device/service api/devices api/devservices +api/diagnostics api/doc api/docs api/documents.list api/documents.search +api/dynamicdata api/edges api/emails api/embed @@ -21063,6 +21159,7 @@ api/monitor api/mpim.list api/nodes api/notice/alert +api/odata api/offline api/oomco_payment/get-failed-transaction-track-ids api/oomco_payment/get-failed-transaction-track-ids-debug @@ -21103,6 +21200,7 @@ api/prodservices api/profile api/profile/gateway api/profile/ping +api/profiler api/profiles/v1/biometric/verify api/profiles/v1/prelogin-check api/projects/search @@ -21123,6 +21221,8 @@ api/queries/search api/query_influxdb api/query_results api/queues +api/quota +api/rate-limit api/rawdata api/reactions.get api/reboot @@ -21138,6 +21238,7 @@ api/recommend/buy/volume api/recommend/list api/recommend/single/list api/recommend/tab +api/redoc api/reference api/refresh api/refresh_access_token @@ -21237,6 +21338,7 @@ api/scenario/name api/scenario/notice api/scenario/ping api/scenario/status +api/schema api/search api/search/character api/search/hot-keyword @@ -21413,6 +21515,7 @@ api/silenceget api/singleclient api/snapshots api/soap +api/soap/wsdl api/spec/swagger.json api/spec/swagger.yaml api/specswagger.json @@ -21478,8 +21581,11 @@ api/swagger/static/index.html api/swagger/staticindex.html api/swagger/swagger api/swagger/swagger-ui.html +api/swagger/ui api/swagger/ui/index api/swagger/uiindex +api/swagger/v2/api-docs +api/swagger/v3/api-docs api/swagger_doc.json api/swaggerindex.html api/swaggerswagger @@ -21522,6 +21628,7 @@ api/testget api/testusers api/themes api/third/videodetail +api/throttle api/timelion/run api/timelionrun api/token @@ -21596,6 +21703,7 @@ api/users-permissions/permissions api/users-permissions/roles api/users.json api/users.list +api/users/count api/users/first-register api/users/me api/users/search @@ -21638,6 +21746,7 @@ api/v1/account/users api/v1/account/users/password api/v1/account/users/summaries api/v1/accounts +api/v1/actuator api/v1/admin api/v1/admin/cron api/v1/admin/orgs @@ -21645,6 +21754,7 @@ api/v1/admin/repos api/v1/admin/users api/v1/alarm-center api/v1/alerts +api/v1/api-docs api/v1/app api/v1/application.wadl api/v1/applications @@ -21652,6 +21762,7 @@ api/v1/archived-workflows api/v1/artifacts api/v1/asset/asset api/v1/asset/assets +api/v1/audit-log api/v1/auth api/v1/authorities api/v1/backend @@ -21663,9 +21774,11 @@ api/v1/broker-admin api/v1/broker/msg api/v1/buckets api/v1/canal/config/1/1 +api/v1/certificates api/v1/channels.list api/v1/cloak api/v1/cluster-workflow-templates +api/v1/clusters api/v1/common/accounts api/v1/common/connections api/v1/common/notifications @@ -21688,19 +21801,23 @@ api/v1/delta/monitoring/accounts api/v1/delta/order api/v1/delta/userAssets api/v1/deploy +api/v1/deployments api/v1/digitalone-cnl-facebook/is-churn api/v1/digitalone-cnl-facebook/post-token api/v1/directory api/v1/discovery api/v1/dns/nameservers api/v1/docs +api/v1/documentation api/v1/employees api/v1/endpoints +api/v1/environments api/v1/event-sources api/v1/events api/v1/exportclients api/v1/fc/bot api/v1/fc/init +api/v1/feature-flags api/v1/files api/v1/finchat-control api/v1/finchat/contact/manager @@ -21725,6 +21842,7 @@ api/v1/healthcheck api/v1/history/history api/v1/importclients api/v1/info +api/v1/integrations api/v1/integrations.list api/v1/keys api/v1/knowledge @@ -21733,6 +21851,7 @@ api/v1/label//values api/v1/label/__name__/values api/v1/label/job/values api/v1/licenses.get +api/v1/limits api/v1/login api/v1/logs/search api/v1/machine @@ -21770,6 +21889,7 @@ api/v1/password api/v1/peers api/v1/permissions.listAll api/v1/persistentvolumes +api/v1/pipelines api/v1/platform api/v1/platform/crypto/public api/v1/platform/server @@ -21789,6 +21909,7 @@ api/v1/query api/v1/query_range api/v1/query_range?query=up&start=1633730000&end=1633733600&step=15s api/v1/quotes +api/v1/rate-limits api/v1/registration api/v1/registry api/v1/remark @@ -21798,6 +21919,7 @@ api/v1/repository api/v1/roles.list api/v1/routes api/v1/rules +api/v1/runner api/v1/savefile api/v1/secrets api/v1/security/csrf_token @@ -21861,8 +21983,11 @@ api/v1/users.list api/v1/users/me api/v1/users/me/settings api/v1/users/search +api/v1/variables api/v1/version +api/v1/webhooks api/v1/workflow-templates +api/v1/workspace api/v1/workspaces api/v1mainMenus api/v1pod @@ -21891,7 +22016,9 @@ api/v2.0/users api/v2/.env api/v2/access api/v2/activity_stream +api/v2/actuator api/v2/admin +api/v2/api-docs api/v2/app api/v2/application.wadl api/v2/auth @@ -21929,8 +22056,11 @@ api/v2/current_user api/v2/current_user/owner api/v2/dashboard api/v2/dashboards +api/v2/deployments api/v2/docs +api/v2/documentation api/v2/explore/tab +api/v2/feature-flags api/v2/feed api/v2/graphiql api/v2/graphql @@ -21940,16 +22070,19 @@ api/v2/healthcheck api/v2/helpdesk/discover api/v2/helpdeskdiscover api/v2/hosts +api/v2/integrations api/v2/inventories api/v2/job_templates api/v2/jobs api/v2/keys +api/v2/limits api/v2/login api/v2/me api/v2/mobile api/v2/organizations api/v2/orgs api/v2/ping +api/v2/pipelines api/v2/playground api/v2/private api/v2/projects @@ -21984,12 +22117,15 @@ api/v2/user/system/versioninfo api/v2/users api/v2/verification/create api/v2/verification/financer +api/v2/webhooks +api/v2/workspace api/v2/workspaces api/v2/write api/v2swagger.json api/v2swagger.yaml api/v3 api/v3/admin/system +api/v3/api-docs api/v3/character/recommend/tag/coll/list api/v3/chat/msg/edit api/v3/chat/regen @@ -21998,6 +22134,7 @@ api/v3/chat/suggestions/buy api/v3/core/applications api/v3/core/tokens api/v3/core/users +api/v3/documentation api/v3/explore/tab/list api/v3/flows/instances api/v3/graphiql @@ -22079,6 +22216,7 @@ api_test api_uru.php apiaccess.log apiaction +apiadmin apialerts apiannotate apiapi @@ -22121,13 +22259,17 @@ apikey apikey/auth.php apikey/authenticate.php apikey/check.php +apikey/create apikey/default.php apikey/home.php apikey/index.htm apikey/index.php +apikey/list apikey/login.php apikey/main.php +apikey/revoke apikey/signin.php +apikey/validate apikey/validate.php apikey/verify.php apikeymanager @@ -22296,6 +22438,7 @@ aplicacion aplicaciones aplio apm +apm/intake/v2/events apm/ui apns apoll @@ -22447,6 +22590,9 @@ app/configparameters.ini app/configparameters.yml app/configroutes.cfg app/configschema.yml +app/Controller/AdminController.php +app/Controller/AppController.php +app/Controller/UsersController.php app/controllers app/dashboard app/dev @@ -22494,6 +22640,8 @@ app/languages.xml app/log app/login app/logs +app/metrics +app/Model/User.php app/models app/phpunit.xml app/private @@ -22522,7 +22670,10 @@ app/unschedule.bat app/vendor app/vendor- app/vendor-src +app/version +app/View/Users app/views +app/webroot/index.php app_ app_admin app_admin.php @@ -22911,6 +23062,8 @@ appraisal/stats.php appraisal/status.php appraisal/view.php appraisal/xml.php +apprise/notify +apprise/stateless/notify approval approval.html approve @@ -22947,6 +23100,7 @@ AppServer.php AppServer.phtml appsettings.config appsettings.json +appsignal AppsLocalLogin AppsLogin appsmith @@ -23028,6 +23182,11 @@ appveyor/stats.php appveyor/status.php appveyor/view.php appveyor/xml.php +appwrite/v1/account +appwrite/v1/databases +appwrite/v1/functions +appwrite/v1/storage +appwrite/v1/users apr apr.html aprcalc @@ -23089,6 +23248,7 @@ arama.html arama.php aran.aspx aranan.php +arangodb aranjuez.html arantius araquote.html @@ -23255,6 +23415,7 @@ argocd/api/v1/repositories argocd/api/v1/session argocd/api/v1/settings argocd/api/version +argocdadmin argomenti argosoft args @@ -23351,6 +23512,8 @@ arte arteddel.php artem2k.html artform.cfm?id= +arthas +arthas/api artho arthritis arthur @@ -23494,6 +23657,7 @@ articulo.php articulos articulos.php artifactory +artifactory/api artifactory/api/build artifactory/api/repositories artifactory/api/search/artifact @@ -23506,6 +23670,7 @@ artifactory/api/storageinfo artifactory/api/system/configuration artifactory/api/system/ping artifactory/api/system/version +artifactory/artifactory/api artifacts artigos artikel @@ -23887,6 +24052,7 @@ assets/config.rb assets/credentials.json assets/fckeditor assets/file +assets/fonts assets/inventory assets/js/fckeditor assets/jsfckeditor @@ -24120,6 +24286,8 @@ attribute Attribute.php attributes attributes/restSearch +attribution +attribution/track AttrTransform.php AttrTypes.php AttrValidator.php @@ -24189,6 +24357,11 @@ audit.html audit.log audit.php audit.txt +audit/events +audit/logs +audit/reports +audit/trail +auditbeat auditevents auditevents.json auditing @@ -24252,6 +24425,9 @@ autentificare.php auteur auth Auth +auth0/callback +auth0/login +auth-bypass auth.asp auth.aspx auth.bak @@ -24288,19 +24464,33 @@ auth/admin/master/console auth/administrator auth/administrator.php auth/apple +auth/apple/callback +auth/auth0/callback +auth/aws-cognito/callback +auth/azure/callback +auth/bitbucket/callback auth/callback auth/catalogue auth/data.php auth/data.xml auth/discord +auth/discord/callback auth/do.php auth/edit.php auth/enter auth/enter.php auth/facebook +auth/facebook/callback auth/github +auth/github/callback +auth/gitlab/callback auth/google +auth/google/callback +auth/jwks +auth/keycloak/callback +auth/keys auth/linkedin +auth/linkedin/callback auth/log.dat auth/log.log auth/log.txt @@ -24314,8 +24504,10 @@ auth/login.shtml auth/logon auth/logon.php auth/microsoft +auth/microsoft/callback auth/oauth auth/oidc +auth/okta/callback auth/panel auth/panel.php auth/pass @@ -24338,9 +24530,11 @@ auth/sign.php auth/signin auth/signin.php auth/slack +auth/slack/callback auth/sso auth/token auth/twitter +auth/twitter/callback auth/v1/admin/users auth/v1/settings auth_ads.php @@ -24398,6 +24592,7 @@ authkey.asp authlog.dat authlog.log authlog.txt +authlogic authlogin authlogin.asp authlogin.html @@ -24560,6 +24755,7 @@ autopass autopilot autoplay.php autopromo +autopsy autoptimize autor autor.php @@ -24787,6 +24983,7 @@ aws/credentials aws/s3/.env aws_credentials aws_keys.txt +awsadmin/config.xml awstat awstats awStats @@ -24885,11 +25082,16 @@ azsdfrtbhnj789 aztecs aztek azure +azure-ad azure-pipelines.yaml azure-pipelines.yml azure/admin azure/console +azuread azureadmin +azureadmin/access.log +azureadmin/capture.php +azurecdn azureus b B @@ -25029,6 +25231,10 @@ back back-end back-end/app/.env back-office +back-office.htm +back-office.php +back-office.phtml +back-office.txt back-office/2fa back-office/about back-office/access @@ -25817,6 +26023,7 @@ backup-2022.zip backup-2023.zip backup-2024.zip backup-all.php +backup-codes backup-data backup-db backup-dir @@ -25893,9 +26100,12 @@ backup/login backup/mysql/.env backup/panel backup/res/.env +backup/restore backup/schedules backup/vendor/phpunit/phpunit backup/vendor/phpunit/phpunit/phpunit +backup_2024.sql +backup_2025.sql backup_db backup_dir backup_entry.cgi @@ -26093,6 +26303,7 @@ bamb bamboo bamboo-specs/bamboo.yml bamboo.yml +bamboo/rest/api/latest bamboo/rest/api/latest/currentUser bamboo/rest/api/latest/plan bamboo/rest/api/latest/project @@ -26640,6 +26851,8 @@ BBApp BBApp.php bbb bbb.html +bbb/api +bbb/bigbluebutton/api bbbb bbbbbb bbboard @@ -26816,6 +27029,7 @@ bearbear bearbeiten bearer.txt bears +beat beater beatles beatrice @@ -26859,6 +27073,7 @@ beds bee beebee beehive +beekeeper BeenThere beer beethoven @@ -27136,6 +27351,7 @@ bfgbuy.php bfgdownload.php bfi bfiles +bfla bfm bg bg4r8y0v.php @@ -27229,6 +27445,7 @@ bigadmin bigb bigbanggravity bigbird +bigbluebutton bigbrother bigbrother.php3 bigdaddy @@ -27346,6 +27563,7 @@ bin.xml bin.zip bin/.env bin/catalina.sh +bin/cli bin/config.sh bin/console bin/contents.htm @@ -27722,6 +27940,7 @@ blazeds/messagebrokerhttp blazeds/messagebrokerhttpsecure blazemeter-index.php blazer +blazer/dashboards blazer/queries blazix blb @@ -27970,6 +28189,7 @@ blogs.html blogs.moderation.php blogs.php blogs/.env +blogs_cat.php blogs_detalle.php blogs_full.php blogs_home.php @@ -28016,6 +28236,7 @@ bluadmin bluadmin.php blue blue365.aspx +blue-green blue/.env blueangel bluebell @@ -28339,6 +28560,7 @@ boke/Edit_Plus/FCKeditor/editor bokep.phtml bokning.html bol +bola boletim boletin boletines @@ -28543,6 +28765,8 @@ booksearch.aspx bookshelf bookshelf.php bookshop +bookstack +bookstack/login bookstep.aspx bookstore bookstore.html @@ -28606,6 +28830,7 @@ border.html Border.php borders borderware +borgbackup boricua boris borkindex.php @@ -28656,6 +28881,7 @@ bots.cnf bots.php botsi botsv +bottle bottom bottom1.php bottom.asp @@ -28869,6 +29095,7 @@ broadboard broadcast broadcast-ip broadcast.php +broadcast/live broadcasting broadcasting/auth broadcasts @@ -29111,6 +29338,7 @@ budget.asp budget.php budgetonline budgettext +budibase/api budlight budweiser buecher @@ -29154,6 +29382,7 @@ bugs.txt bugs.xml bugs/verify.php?confirm_hash=&id=1 bugsbunny +bugsnag bugsverify.php?confirm_hash=&id=1 bugtrack bugtracker @@ -29261,6 +29490,7 @@ bulk.php bulkadd.asp bulkdiscounts.asp bulkemail +bulkhead bulkmail bulkquery bulksms @@ -29301,6 +29531,7 @@ bunka bunnies bunny bunnyboo +bunnycdn bunnyslippers buoni-sconto bupa @@ -29321,6 +29552,7 @@ burner burningwhee1s burnwave, burp +burpsuite burst burst.html burtchen @@ -29902,6 +30134,7 @@ cabinet/withdrawals cabinet/workers cabinets cabins +cable cabletron caboose cabrera @@ -29927,8 +30160,21 @@ cache.html cache.old Cache.php cache.php +cache/clear +cache/flush cache/index.php +cache/info +cache/keys +cache/prewarm +cache/purge +cache/shell.php cache/sql_error_latest.cgi +cache/stats +cache/status +cache/tags +cache/v1/purge +cache/v2/purge +cache/warm cache_archiver.php cache_bbcodes.php cache_birthdays.php @@ -30196,6 +30442,7 @@ calhead.html cali caliban calibration.php +calicoadmin/ips.log calife californ california @@ -30235,6 +30482,7 @@ callback.phtml callback_form.php callback_mb.php callback_url.php +callbackadmin callcenter callcenter.php callee @@ -30356,6 +30604,8 @@ canada.php canadapost.php canal canales +canary +cancancan canced cancel Cancel @@ -30848,6 +31098,7 @@ casio casper caspsamp cassandra +cassandra-admin cassidy cassie Cassini.exe.config @@ -30933,8 +31184,10 @@ catalog/admin.php catalog/admin/login.php catalog/administator.php catalog/api +catalog/language/en-gb/common catalog/login.php catalog/login_diz.php +catalog/model/account catalog/product.asp?cat_id= catalog/product.asp?pid= catalog/viewtheme @@ -31309,6 +31562,13 @@ cdkey.txt cdm_ggao_tiezi.asp cdma cdn +cdn-cgi +cdn-cgi/beacon/performance +cdn-cgi/challenge-platform +cdn-cgi/l/chk_jschl +cdn-cgi/rum +cdn-cgi/specials/wrk +cdn-cgi/trace cdn/assets cdn/static cdomain @@ -31506,6 +31766,7 @@ certkey.asp certprov certprovlocalhost certs +certs/config certs/server.key certserv certserver @@ -31530,6 +31791,8 @@ cet cetelem cev cf +cf-cache-status +cf-ray cf_bulletin.cfc cf_calendar.cfm cf_nuke @@ -31679,23 +31942,39 @@ cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etcpasswd cgi-bin/.env cgi-bin/a1stats/a1disp.cgi cgi-bin/a1statsa1disp.cgi +cgi-bin/admin cgi-bin/awstats cgi-bin/awstats.pl +cgi-bin/backup cgi-bin/bugzilla +cgi-bin/config cgi-bin/config.exp +cgi-bin/device +cgi-bin/dhcp +cgi-bin/diag +cgi-bin/dns cgi-bin/error_log +cgi-bin/factory +cgi-bin/firewall +cgi-bin/firmware +cgi-bin/home cgi-bin/htimage.exe cgi-bin/htimage.exe?2,2 cgi-bin/htmlscript cgi-bin/imagemap.exe cgi-bin/imagemap.exe?2,2 cgi-bin/index.html +cgi-bin/info +cgi-bin/lan +cgi-bin/log cgi-bin/logi.php cgi-bin/login cgi-bin/login.cgi cgi-bin/login.php cgi-bin/loginasp cgi-bin/loginphp +cgi-bin/luci +cgi-bin/main cgi-bin/mt7/mt-xmlrpc.cgi cgi-bin/mt7/mt.cgi cgi-bin/mt7mt-xmlrpc.cgi @@ -31708,14 +31987,35 @@ cgi-bin/mtmt-xmlrpc.cgi cgi-bin/mtmt.cgi cgi-bin/nagios3/status.cgi cgi-bin/nagios4/status.cgi +cgi-bin/nat +cgi-bin/network +cgi-bin/passwd cgi-bin/php cgi-bin/php.ini +cgi-bin/ping cgi-bin/printenv cgi-bin/printenv.pl +cgi-bin/reboot +cgi-bin/reset +cgi-bin/restore +cgi-bin/settings +cgi-bin/snmp +cgi-bin/status +cgi-bin/syslog +cgi-bin/system +cgi-bin/telnet cgi-bin/test-cgi cgi-bin/test.cgi +cgi-bin/traceroute +cgi-bin/upload cgi-bin/upload/web-ftp.cgi +cgi-bin/user cgi-bin/ViewLog.asp +cgi-bin/vpn +cgi-bin/wan +cgi-bin/webproc +cgi-bin/wifi +cgi-bin/wireless cgi-bin_ssl cgi-binawstats.pl cgi-binerror_log @@ -31812,6 +32112,7 @@ Cgishell.phtml Cgishell.pl cgishl CgiStart?page=Single +cgit cgitelnet.php cgiwin cgiwrap @@ -32185,12 +32486,14 @@ chartFile_Rev1.php chartimg.axd ChartImg.axd charting +chartmuseum charts charts-min.js charts.aspx charts.cfm charts.html charts.php +charts/index.yaml charts/liveObjects/.env charts_library chartSettings @@ -32772,6 +33075,7 @@ chronic chronicle chronik chrono24 +chronograf chs CHS cht @@ -32879,6 +33183,11 @@ circeos circle circle.yml circleci +circleciadmin +circleciadmin.sh +circuit +circuit-breaker +circuitbreaker circuits cirkuitincludes cis @@ -33173,6 +33482,7 @@ classes/cookie.txt classes/db/DbPDO.php classes/gladius/README.TXT classes/gladiusREADME.TXT +classes/PaymentModule.php classes/SiteMap.php classes/upload/changes.txt classes/upload/documentation.htm @@ -33196,6 +33506,7 @@ classic.html classic.json classic.jsonp classical +classicapi/doc classics classificados classification @@ -33301,6 +33612,8 @@ clever clf cli cli.php +cli/execute +cli/run clic clic.asp clic.php @@ -33349,7 +33662,10 @@ clickedon clicker.php clickheat clickhere.aspx +clickhouse +clickhouse/query clickinfo +clickjacking clickme.cgi clickme.php clicknbuild @@ -33890,9 +34206,13 @@ cloud-hosting.php cloud-provider.yaml cloud.php cloud/admin +cloud/api cloud/console cloud_theme +cloudadmin cloudbank/detail.asp?ID= +cloudbeaver +cloudbeaver/api clouder.asp cloudexp/application/configs/application.ini cloudflare @@ -33901,6 +34221,7 @@ cloudflare/admin cloudflare_credentials.txt cloudformation cloudfoundryapplication +cloudfront cloudmonitor.log.20210417 cloudmonitor.log.20210418 cloudmonitor.log.20210419 @@ -33911,6 +34232,7 @@ cloudnine clouds cloudstore/configmysql.xml clover +clover.xml clp cls cls.php @@ -33959,6 +34281,8 @@ clubsinfo cluecentral clueless cluster +cluster/admin +cluster/api cluster/cluster cluster/config cluster/healthcheck @@ -34575,6 +34899,7 @@ codemasters codenames-frontend/.env codepages codepress +codereview codes codes.php codesearch @@ -34599,6 +34924,9 @@ coformat.txt coger cogito cognates.pdf +cognito +cognito/callback +cognito/login cognos coi coid @@ -34623,6 +34951,8 @@ colin coll_info collab collab-connect-web-application/server/.env +collabora +collabora/api collaboration collabtive collapse @@ -34910,6 +35240,7 @@ commander.php commandes commandfile Commands +commands commands.html commands.php commandshell.inc @@ -35030,6 +35361,7 @@ commissions commit commit.php COMMIT_EDITMSG +commits committed.html committee committee.php @@ -35302,6 +35634,7 @@ completeorder.aspx completesetup.php compliance compliance.php +compliance/reports complicated comply component @@ -35327,6 +35660,8 @@ components/com_foxcontact/up.php components/com_foxcontact/upload.php components/com_hdflvplayer/hdflvplayer/download.php components/com_hdflvplayer/hdflvplayer/unduh.php +components/com_jce +components/com_jce/licence.txt components/com_users/views components/login components/login.ascx @@ -35729,6 +36064,9 @@ config/app.yml config/AppData.config config/auth.php config/autoload +config/autoload/global.php +config/autoload/local.php +config/autoload/local.php.dist config/aws.yml config/banned_words.txt config/cable.yml @@ -35740,8 +36078,10 @@ config/config.toml config/config.yaml config/config.yml config/configuration.yml +config/core.php config/cucumber.yml config/database.php +config/database.php.default config/database.yaml config/database.yml config/database.yml.enc @@ -35754,6 +36094,7 @@ config/db.inc config/development config/elasticsearch.yml config/environment.rb +config/environments/development.rb config/error_log config/initializers/secret_token.rb config/initializerssecret_token.rb @@ -35783,6 +36124,7 @@ config/properties.ini config/push_ssh_keys.yml config/push_ssh_keys_remote.yml config/routes +config/routes.php config/routes.yml config/s3.yml config/secrets.yml @@ -35820,6 +36162,7 @@ config_add_news.php config_ads.php config_backup.php config_backup.sql +config_backup.tar.gz config_cache.php config_clicks.php config_cust.php @@ -36024,6 +36367,7 @@ conflocalhost conflogging.properties confluence confluence/admin +confluence/login.action confluence/pages/listpermissionpages.action confluence/pages/templates/createpagetemplate.action confluence/pages/templates/listpagetemplates.action @@ -36048,6 +36392,9 @@ confluence/plugins/servlet/oauthview-consumer-info confluence/plugins/servlet/upm confluence/plugins/servletembedded-crowd confluence/plugins/servletupm +confluence/rest/api/content +confluence/rest/api/space +confluence/rest/api/user confluence/spaces/addmailaccount.action confluence/spaces/exportspacehtml.action confluence/spaces/exportspacexml.action @@ -36105,9 +36452,15 @@ connect.php.bak connect.php~ connect/authorize connect/endsession +connect/facebook +connect/github +connect/google connect/introspect +connect/linkedin +connect/microsoft connect/revoke connect/token +connect/twitter connect/userinfo connect_db.php connect_old.php @@ -36174,6 +36527,7 @@ console/activity console/admin console/aml console/analytics +console/api console/api-keys console/api/api-keys console/api/apps @@ -36204,6 +36558,7 @@ console/categories console/cd console/ci console/clients +console/commands console/comments console/compliance console/config @@ -36757,6 +37112,7 @@ content-builder content-form content-images content-manager +content-type-builder content../.git/config content..json content.asp @@ -37058,6 +37414,8 @@ control-panel/version control-panel/webhooks control-panel/withdrawals control-panel/workers +control-room.php +control-room.phtml control.asp control.aspx control.dat @@ -37237,6 +37595,9 @@ control_light.php control_panel control_panel.php control_panel/login +control_room.htm +control_room.php +control_room.phtml controladmin controlBase controlcenter @@ -37510,6 +37871,7 @@ conversion.asp conversion.htm conversion.html conversion.php +conversion/track conversions convert convert-uulib @@ -37612,6 +37974,7 @@ coolsettings.cfm coolsite.cfm coolsites coolstuff.cfm +coolwsd cooolsoft coop COOP @@ -37789,9 +38152,11 @@ correo.php correoweb correspondence correu +cors corsi.php cortafuegos cortex +cortex/api/responder cortez coruna coruna,-a.html @@ -37827,7 +38192,10 @@ cottage cottage.html cottages cotton +coturn couchdb +couchdb/_all_dbs +couchdb/_session coucou.php cougar cougars @@ -38342,7 +38710,9 @@ CramMD5.php crap crapp crash +crash-reporting crash.log +crash/report crashes crawford crawl @@ -38354,6 +38724,7 @@ crawlprotect crawltrack cray crazy4u +crazy-egg crazygirl crc crc.corp.footer @@ -38643,6 +39014,8 @@ cronout2.php cronout.php cronrun.php crons +crons/admin +crons/list cronscripts crontab crontabs @@ -38680,6 +39053,7 @@ crosswords crot.phtml crotz.phtml crow +crowd crowd/console crowd/console/login.action crowd/consolelogin.action @@ -38701,6 +39075,7 @@ crss crtemplate.html crtr cru +crucible cruise cruise-holidays cruise-lines.php @@ -38808,6 +39183,7 @@ csm CSMA cso csp +csp-report csp/gateway/slc/api/swagger-ui.html csp/gateway/slc/apiswagger-ui.html csproj @@ -39553,6 +39929,8 @@ cv_rss_feeds.php cvc2.htm cvc2.html cvc.html +cve +cve-report cvnhelp.aspx cvnhelp.aspx.cs cvs @@ -39771,6 +40149,8 @@ dadmin.php dados daemon daemon.php +daemons +daemons/admin daffodil dagit dagit/graphql @@ -39903,6 +40283,7 @@ daryl das dasbhoard dash +dash/live dasha DASHBOARD dashboard @@ -40303,6 +40684,7 @@ data8888 data%23data.asp data-api.php data-files +data-layer data-nseries.tsv data-protection.asp data.7z @@ -40865,6 +41247,7 @@ datadm.asp datadm.aspx datadm.php datadog +datadog/intake datadump dataenter.php dataentry @@ -40903,6 +41286,7 @@ dataimages.php DataImport datakey datakommunikation +datalayer datalex dataLib.class.php datalogh.dtb @@ -41229,6 +41613,7 @@ db/dump db/dump.sql db/dumper.php db/enter.php +db/h2-console db/index.php db/item.html?item= db/login.php @@ -41273,6 +41658,8 @@ db_14.php db_15.php db_16.jsp db_16.php +db_2024.sql +db_2025.sql db__.init.php db_access.php db_admin @@ -41408,6 +41795,7 @@ dbadmin.tar.gzip dbadmin.tgz dbadmin.zip dbadmin/account.php +dbadmin/csv.php dbadmin/index.php dbadminaccount.php dbadminindex.php @@ -41494,6 +41882,7 @@ dbdump.tgz dbdump.zip dbdumper.php dbdumps +dbeaver dbeditor dbef.php dbenter.php @@ -41507,6 +41896,7 @@ dbforms dbg dbg-wizard.php DBG.php +dbgate dbhost dbhotlink.php dbi @@ -42069,6 +42459,8 @@ delaware delaware.html delay delayed_job +delayed_job/failed +delayed_job/jobs delaylink.php delcart.php delcomment.php @@ -42481,6 +42873,7 @@ descs desctracker.php desenvolupament desenvolvimento +deserialization deserializer.php desgetfiles.aspx desi-hits.php @@ -42789,6 +43182,7 @@ devin Devine1 devis devis.php +devise devlink.php devlnull devnet @@ -42825,6 +43219,7 @@ dfa dfdf.php dfile.ashx dfile.php +dfir-iris dfnet dforum dfp_cookie.aspx @@ -42840,6 +43235,8 @@ dgidc2.php dgidc-2.php dgidc.php dgj +dgraph +dgraph/admin dgssearch dh dh_ @@ -42970,10 +43367,12 @@ diff.php difference difference.html difference.php +differential difficulty DiffieHellman.php diffs difftime.php +diffusion dig dig.asp dig_exhib.php @@ -43085,6 +43484,7 @@ diradminauth.php diradminlogin.php dirb dirbmark +dirbuster direct direct1 direct.htm @@ -43161,10 +43561,18 @@ directoryadmin.html directoryadmin.php directredirect.php directtopics +directus +directus/activity directus/collections directus/fields +directus/files +directus/permissions +directus/relations +directus/revisions +directus/roles directus/server/health directus/server/info +directus/settings directus/users directv directvdsl @@ -43205,6 +43613,7 @@ disappear disappearing disassembler disaster +disaster-recovery disc disc.asp disc.php @@ -43400,7 +43809,9 @@ disputes disseny dist dist.php +dist.tar.gz dist/.env +dist/fonts distance distance.php distancelearning @@ -43483,6 +43894,7 @@ dk dk-de dk-gb dkdk-myoffice.html +DKIM dkms.conf dkscript dl @@ -43529,6 +43941,7 @@ dlr_jasmin.php dls dlstats_nbulker.php dlt +DLUsers dm dm-config dm.asp @@ -43537,6 +43950,7 @@ dm.html dm.php dmail dmanews +dmarc dmc dmca dmca-notice.html @@ -43591,6 +44005,7 @@ DNS.php dns.php dns/admin dns/zones +dnsadmin dnsmasq dnstools dnt @@ -43877,6 +44292,9 @@ documentation.rst Documentation.txt documentation.txt documentation/config.yml +documentation/swagger-ui +documentation/swagger.yaml +documentation/swagger.yml documentationconfig.yml Documentationreadme.txt documentazione @@ -43909,6 +44327,8 @@ documents.jsf documents.php documents.vb documentum +docusaurus +docusaurus/api docuwiki dodaj-komentarz.php dodaj-strone @@ -44087,6 +44507,7 @@ doomsday door door.php door.phtml +doorkeeper doors doorway doow @@ -44388,6 +44809,7 @@ dragresizetable drama drama.html dramaqueen +dramatiq dratfs draver draw @@ -44465,6 +44887,7 @@ drm_unpaiditem.php droelf.kit drone/api/repos drone/api/user +droneadmin drop drop.php drop_post.php @@ -44545,6 +44968,7 @@ Drupal.php drupal.php drupal/admin drupal/user/login +drupaladmin drupalit drushrc.php drv @@ -44602,8 +45026,10 @@ dtlimg.php dtlink dtmcms dtmp +dto dtool_pro.php DTool_Pro.php +dtos dtp dtproperties dtr @@ -44616,6 +45042,7 @@ du dual duanereade dubai +dubbo dubbo-admin dubee dubelu @@ -44689,6 +45116,9 @@ dump.zip dump/admin dump/admin.php dump/backup +dump_2024.sql +dump_2025.sql +dump_2026.sql dumpadmin.php dumpenv dumper @@ -44719,12 +45149,15 @@ dunzip.php duo duo.html duo.php +duo/auth +duo/callback dupfiles duplicate Duplicate.php duplicate.php duplicate_rows.php duplicateFolderCases.php +duplicati dupont durango duration @@ -45267,6 +45700,9 @@ edenred edgar edgardo edge +edge/cache +edge/purge +edgecast edges edgewall edgy @@ -46468,6 +46904,10 @@ elaine elamor elanor elastic +elastic-apm-agent +elastic/security +elastic/siem +elasticadmin elasticsearch elasticsearch/_mapping elasticsearch/_nodes @@ -47626,6 +48066,8 @@ engine/classes/swfuploadswfupload_f9.swf engine/log.txt engine/login engine/login.php +engine/opensearch.php +engine/search.php engine/sign engine/sign.php engine/signin @@ -47970,6 +48412,8 @@ epdqout.php epg ephotos epic +episerver +episerver/cms episerver/cms/admin episode episodes @@ -48200,6 +48644,7 @@ error-page.html error-page.php error-pages error-send.html +error-tracking error. error.7z error.2021-04-07.log @@ -48236,6 +48681,7 @@ error.xml error.zip error/.env error/error.log +error/report error_ error_403.htm error_403.html @@ -48399,7 +48845,9 @@ esbit.php esborrar esc escalate_issue.jsp +escalation escalation.pdf +escalations escapades escape escape.html @@ -48659,6 +49107,7 @@ eunomia eupdate euphoria.html eur +eureka eureka/apps eurl.axd euro @@ -48724,6 +49173,7 @@ evenements.php evennews_blocks.php event Event +event-bus event-calendar.html event-info.aspx event-map.asp @@ -48750,6 +49200,7 @@ event_listing.jsp event_post.php eventalbums.aspx eventbrite +eventbus eventcal eventcalendar eventdata @@ -48808,6 +49259,7 @@ events/_search events/detail.asp?ID= events/event-detail.cfm?intNewsEventsID= events/event.asp?id= +events/handlers events/index events/restSearch events/stream @@ -48927,6 +49379,7 @@ exampleapp exampledir examples examples.html +examples.md examples.php examples/01-simple-model/.env examples/02-complex-example/.env @@ -49010,6 +49463,7 @@ exception.cfm exception.log Exception.php exception.php +exception/report exception_log exception_log.txt exceptionerror.cfm @@ -49159,6 +49613,7 @@ experiences experiment experimental experiments +experiments/results expert expert.aspx expert.html @@ -49447,6 +49902,7 @@ extras extras.aspx extras.htm extras.html +extras.md extras.php extras/django_bash_completion extras/documentation @@ -49589,10 +50045,13 @@ facebox facedisc.html facefiles faces +faces/admin +faces/index.xhtml faces/javax.faces.resource/web.xml?ln=../WEB-INF faces/javax.faces.resource/web.xml?ln=..\\WEB-INF faces/javax.faces.resource/web.xml?ln=..WEB-INF faces/javax.faces.resourceweb.xml?ln=..\\WEB-INF +faces/login.xhtml facestones facetalkonline.php facileforms @@ -49653,6 +50112,7 @@ failed failed.htm failed.php failed_auth.html +failover failure failure-print.htm failure.asp @@ -49682,6 +50142,7 @@ falcons falconseye fall fall04.pdf +fallback fallback-reboot fallen falloutboy @@ -49844,6 +50305,7 @@ fashion_mosaic fasoo fast Fast_Lane_Checkout +fastapi fastbin fastfind fastforward @@ -49863,6 +50325,8 @@ fastlanescreenshots fastlanetest_output fastlink fastloads +fastly/purge +fastly/softpurge fastorder.aspx fastphp.ini fastpublish @@ -49887,6 +50351,8 @@ fatboy fatcat fatcow.php father +fathom +fathom/collect fatih1 fatman fatsecret @@ -50161,6 +50627,7 @@ feature.cfm feature.html feature.php feature.xml +feature/toggle feature_flags feature_list.php feature_request.jsp @@ -50378,6 +50845,7 @@ fffua.php ffmpeg ffr_cart.html fftw +ffuf fg fg_email_signup.htm fg_shopfromcat.html @@ -50558,6 +51026,7 @@ fileajaxUpload filearchive filebase filebased +filebeat filebin filebrowser/api/login filebrowser/api/settings @@ -50740,6 +51209,7 @@ files_13.php files_14.php files_15.php files_16.php +files_backup.tar.gz files_deleted files_log filesadmin.asp @@ -50841,6 +51311,8 @@ filessa.asp filessa.aspx filessa.php Filessearch.indexes +filestash +filestash/login filestest.asp filestest.aspx filestest.php @@ -50944,7 +51416,7 @@ filter.aspx filter.html Filter.php filter.php -filter/jmol/iframe.php?_USE=%22};alert(1337); +filter/jmol/iframe.php filter/jmol/js/jsmol/php/jsmol.php?call=getRawDataFromDatabase&query=file filter/jmol/js/jsmol/phpjsmol.php?call=getRawDataFromDatabase&query=file filterinput.php @@ -51155,6 +51627,7 @@ firmen_export.php firms firms.php firmware +firmware/update firmy first first4internet @@ -51275,6 +51748,7 @@ flags flags.js.php flags.json flags.php +flags/admin flags/api flags/config flags/status @@ -51345,6 +51819,7 @@ flashgames flashindex.html flashobject.js flashpeak +flashphoner flashplayer flashs flashservices @@ -51495,8 +51970,11 @@ flowable-admin/app/rest/server-configs flowable-rest/service/management/engine flower flower.htm +flower/api flower/api/tasks flower/api/workers +flower/dashboard +flower/workers flowerfire flowerpower flowers @@ -51516,11 +51994,13 @@ flsh flu fluege fluent-bit +fluent-bit/api fluent-bit/api/v1/health fluent-bit/api/v1/metrics fluent.conf fluent_aggregator.conf fluentd +fluentd/api fluffy flughafenausbau fluidgames @@ -51651,6 +52131,7 @@ follow follow.html follow.php follow_link.php +follower followers following.php followup @@ -51805,11 +52286,15 @@ foresee foresight forest foretag +forever +forever/list forevernew forex forex.html forfaits.php forge +forgejo +forgejo/user/login forget forget.html forget.php @@ -52981,6 +53466,7 @@ fullsitemap.aspx fullsizecover fullsizegame FullStory.asp?Id= +fullstory/rec/bundle fulltext fulltext.php fulltextsearch.asp @@ -53169,6 +53655,7 @@ futuresoft futurestudents futurewave fuzz +fuzzing fuzzy_seofq fuzzymonkey fv @@ -53566,6 +54053,8 @@ gateway.htm gateway.html gateway.php gateway/.env +gateway/filters +gateway/refresh gateway/routes gatewayroutes gateways @@ -53656,6 +54145,7 @@ gcp gcp/admin gcp/console gcpayment +gcpcdn gcprocessipn.asp gcs_templates gcshared @@ -53928,11 +54418,13 @@ gernot geronimo gerrard gerrit +gerrit/a/changes gerrit/accounts/self gerrit/changes gerrit/config/server/info gerrit/config/server/version gerrit/groups +gerrit/login gerrit/projects gerson gert @@ -54321,6 +54813,7 @@ getSexo.php getShare.php getsiteversion.asp getsnap.php +GetSnapshot getsolutions GetSpeedAlerts.php GetSpeedLimit.php @@ -54434,6 +54927,10 @@ ghost/api/admin/site ghost/api/admin/users ghost/api/content/posts ghost/api/content/settings +ghost/api/v4/admin +ghost/api/v4/admin/settings +ghost/api/v4/admin/users +ghost/api/v4/content ghost/signin ghostdriver.log ghosts @@ -54575,19 +55072,32 @@ git/index git/logs/HEAD git/packed-refs git/refs/heads/master +gitbook gitea +gitea/api/v1/admin/users +gitea/api/v1/repos/search +gitea/api/v1/settings/api +gitea/api/v1/users/search +gitea/user/login github github-cache github-connect/.env github-recovery-codes.txt +github/api github_token.txt gitlab gitlab/admin +gitlab/api/v4/admin/users +gitlab/api/v4/groups +gitlab/api/v4/projects +gitlab/api/v4/users +gitlab/explore gitlog gitlog.dat gitpull.php gitweb gitweb.cgi +gitweb/?p=.git;a=summary give give.php giveadmin @@ -54884,12 +55394,14 @@ goaway.php goback goblue gobo.jsp +gobuster gocart.asp gocc gococo.php gocougs gocr god.php +god/status god_admin godaddy godbless @@ -54917,6 +55429,8 @@ gogogo gograboid.php gogreen.aspx gogs +gogs/api/v1/users/search +gogs/user/login gohome.js gohome.php gohomeframe.js @@ -55141,6 +55655,8 @@ gotcha gotcha.html gotdotnet gothic +gotify +gotify/message gotlinks.php gotmilk goto @@ -55915,9 +56431,11 @@ graf.php grafana grafana/api/admin/settings grafana/api/admin/stats +grafana/api/admin/users grafana/api/alerting grafana/api/annotations grafana/api/dashboards +grafana/api/dashboards/home grafana/api/datasources grafana/api/health grafana/api/org @@ -55971,6 +56489,7 @@ graph/query graph_cms graph_ex.php graph_update.php +graphene graphfactory.php graphic graphic-design @@ -55991,6 +56510,8 @@ graphiql/finland graphiqlfinland graphique.php graphite +graphite/metrics/find +graphite/render graphix graphjpgraph.php graphon @@ -56124,6 +56645,7 @@ grill grimm gringotts grip +grist/api groceries grok grokster @@ -56223,8 +56745,11 @@ growthbook/api/experiments growthbook/api/features grp GRP_WebShell.php +grpc +grpc-web grpc.health.v1.Health/Check grpc.reflection.v1.ServerReflection/ServerReflectionInfo +grr grs grsecurity grube @@ -56275,6 +56800,7 @@ gt gt-cache gt.php gta +gtag/js gtasoft gtcatalog gtchat @@ -56321,6 +56847,7 @@ guard.php guard.php.bak guard_nwcontent.php guardar +guarded guardednet Guardfile guardian @@ -56546,6 +57073,7 @@ h1.php h2 h2-console h2-h3.php +h2/console h2console h2opolo h4.php @@ -56721,6 +57249,10 @@ handys hangar-16.html hangaroo hangfire +hangfire/jobs +hangfire/recurring +hangfire/servers +hangfire/stats hangman hangman.php hanks @@ -56827,6 +57359,7 @@ haskell.php haslo.php hassan hastymail +hasura hasura/.env hat hata.asp @@ -57007,6 +57540,7 @@ healthCheck healthcheck.cfm healthcheck.html healthcheck.php +healthchecks-ui healthe-plex.asp healthe-pulse.asp healthe-shield.asp @@ -57014,10 +57548,13 @@ healthnotes.aspx healthprofile healthyyou.html healthz +heap/identify +heap/track heapdump heapdump.json heart heart-disease2.aspx +heartbeat heartbeat.aspx heartbeat.php heartbreaker @@ -57029,6 +57566,7 @@ heat.html heather heating-system.html heatmap +heatmap-data heatmaps heaven.html heavenly @@ -57108,6 +57646,7 @@ HelloWorldServlet HelloWorldServlet.jsp helly-hansen.php helm +helm/charts helm/values.prod.yaml helm/values.staging.yaml helm/values.yaml @@ -57202,6 +57741,8 @@ helperfiles helpers helpers.php helpers.php.bak +helpers/admin +helpers/debug helpfiles helpframe.aspx helpful @@ -57523,6 +58064,9 @@ hledej_2.php hln_index.jsp hloader hlp +hls +hls/live +hls/stream hlstats hlstats.php hlsw @@ -57771,6 +58315,7 @@ honest honesty honey honey.html +honeybadger honeybee honeycard.aspx honeyd @@ -57988,6 +58533,7 @@ hotelxml hotfoon hotfrog hotgirl +hotjar hotjobs hotline hotline.php @@ -58022,6 +58568,7 @@ hottopics.aspx.cs hottrends hotufi2 hotwebscripts +hotwire hou houdini.php hound @@ -58400,6 +58947,8 @@ hubble/v1/flows hubble/v1/nodes hubble/v1/status hubicka +hubs +hubs/chat hubspot-lead2.php hubspot-leads-all.php hubspot-leads-old.php @@ -58413,6 +58962,7 @@ hudsonlogin huedqz0d.php huelva huelva.html +huey huggel huggies huggiesau @@ -58817,6 +59367,7 @@ idmelden2.php idmelden.php idn idontknow +idor idp ids ids.log @@ -59875,6 +60426,8 @@ incerror_log incfacebook.php incfckeditor incfiles +incident +incidents incimages incl incl_db.php @@ -59965,6 +60518,7 @@ includes/admins.asp includes/admins.php includes/adovbs.inc includes/amad.php +includes/application_top.php includes/asp.aspx includes/awstats includes/b.php @@ -59983,6 +60537,7 @@ includes/cgi.pl includes/Cgishell.pl includes/changeall.php includes/configuration.php +includes/configure.php includes/configure.php~ includes/cp.php includes/cpanel @@ -59995,6 +60550,7 @@ includes/d.php includes/dam.php includes/data.sql includes/database/install +includes/database_tables.php includes/dom.php includes/downloads/dom.php includes/dz0.php @@ -60136,6 +60692,7 @@ includes/useradmin includes/vb.rar includes/vb.sql includes/vb.zip +includes/version.php includes/webadmin.html includes/whmcs.php includes/WolF.php @@ -60547,6 +61104,10 @@ index.php.tmp index.php.txt index.php.wordpress index.php.zip +index.php/apps/files +index.php/login +index.php/logout +index.php/settings/admin index.php_ index.php_bak index.php_bk @@ -60868,6 +61429,8 @@ indexoriginal.php indexorjj.php indexphil.php indexpic.asp +indexpoisk +indexpoisk.php indexppc.cfm indexprint.php indexprocess.cfm @@ -60915,6 +61478,8 @@ indiana.php indianapolis indiaplaza indiatimes +indicator +indicators indice indice.html indiceizda.htm @@ -60995,7 +61560,9 @@ infinitesoftware infiniti infinity inflation-print.htm +influx influxdb +influxdb/query info Info info1.html @@ -61495,6 +62062,7 @@ inscription_oa.php inscriptioncli.srvl inscriptions insead.files +insecure-direct-object insenz.func.php insenz.inc.php insenz.php @@ -61673,6 +62241,7 @@ install/adminstrator install/adminstrator.php install/enter install/enter.php +install/includes install/index.php install/index.php?upgrade install/install.sh @@ -61722,8 +62291,8 @@ installation.aspx installation.back.php installation.htm installation.html -INSTALLATION.md installation.md +INSTALLATION.md installation.old installation.old.php installation.php @@ -61768,6 +62337,7 @@ installupdate.log installweb.config installwordpress install~ +instana/trace instance instance/config.py instancefiles @@ -61864,6 +62434,7 @@ interceptor.php interceptors interchange Interchange.php +intercom interdit.php interdoc interesados.php @@ -62168,7 +62739,9 @@ inv invalid invalid.html invalid_login.asp +invalidate invalidatecache.asp +invalidation invalidcc.asp InvalidPromotion invalidrequest.aspx @@ -62271,6 +62844,8 @@ io io.php io.swf ioana +ioc +iocs iodine iohat604.php iol @@ -62967,6 +63542,8 @@ janles_mkr.htm janles_new.cgi janr.php januari +janus +janus/admin jap japan japan.html @@ -63304,10 +63881,18 @@ jingjing jingying jinzora jira +jira-service-management +jira/plugins/servlet/gadgets +jira/rest/api/2/issue +jira/rest/api/2/project +jira/rest/api/2/user jira/secure/Dashboard.jspa jira/secureDashboard.jspa jiran jiros +jitsi +jitsi-meet +jitsi/login jitterbug jiudian jive @@ -63490,8 +64075,11 @@ jobs.htm jobs.html jobs.php jobs.xml +jobs/admin +jobs/list jobs/send-email jobs/send-notification +jobs/status jobs_no.html jobsearch jobsearch.asp @@ -63599,6 +64187,7 @@ jolokia/exec/ch.qos.logback.classic jolokia/execch.qos.logback.classic jolokia/list jolokia/read +jolokia/search jolokia/version jolokialist jolokiaversion @@ -63694,6 +64283,8 @@ journal journal.asp journal.cgi journal.php +journal/content_list.php +journal/nlist.php journal_proc.php journaleditors.aspx journalgetpage.aspx @@ -63962,6 +64553,7 @@ jsmenu json json-api json-min.js +json-rpc json.ini json.js JSON.php @@ -63978,6 +64570,7 @@ jsonlib.php jsonp jsonp.php jsonpost.php +jsonrpc jsonwrapper jsoutput jsp @@ -64126,6 +64719,7 @@ jump.php jump.php3 jump.phtml jumpauction.php +jumpcloud jumper.php jumphot.php Jumping.php @@ -64192,14 +64786,18 @@ jvs jvtools.html jw jwc +jwks jwks.json jwks.jwt jwplayer jws jwsdir +jwt jwt.key jwt.php +jwt/decode jwt/private.pem +jwt/verify jwt_secret.txt jwtTest.php jx44146l.php @@ -64342,6 +64940,7 @@ kansascity kanto kantoor kaosjs +kapacitor kapcsolat kapcsolat.html kaphotoservice @@ -64586,15 +65185,24 @@ key.php key.php.encryptd Key.txt key.txt +key/create +key/list +key/revoke key_assoc.php key_assoc.php3 key_form.jsp key_set.jsp keyadmin keyadmin.php +keybase.txt +keybase/config keyboard keyboard.asp +keycdn +keycloak keycloak.json +keycloak/auth +keycloak/realms/master keydetails keye keyence @@ -64612,7 +65220,10 @@ keys.php keys.txt keys.yaml keys.yml +keys/active +keys/all keystone +keystone/api keystore.jks keystore.pfx keystring @@ -64670,11 +65281,23 @@ kiara kiax kibana kibana/.env +kibana/api/fleet/agents +kibana/api/fleet/enrollment-api-keys kibana/api/saved_objects kibana/api/spaces kibana/api/status kibana/app +kibana/app/apm +kibana/app/dashboards +kibana/app/discover +kibana/app/fleet kibana/app/kibana +kibana/app/maps +kibana/app/ml +kibana/app/osquery +kibana/app/security +kibana/app/visualize +kibana/login kibanalogin.asp kibanalogin.jsp kibanalogin.php @@ -64708,6 +65331,7 @@ kijelentkezes.php kikay Kikou7734 kill +kill-switch kill.cgi killa killbill @@ -64918,6 +65542,7 @@ kokeshcms koko kolab kolab-syncroton/.env +kolide komentar-new.php komentar.php komentar_new.php @@ -65132,7 +65757,9 @@ kube kube-apiserver.log kube-controller-manager.log kube-proxy.log +kube-public kube-scheduler.log +kube-system kubeflow/jupyter kubeflow/pipeline kubeflow/pipeline/apis/v1beta1/pipelines @@ -65182,6 +65809,8 @@ kupon kupon.php Kurama.php kuratorium +kurento +kurento/api kurgan kurs kurs.php @@ -65357,6 +65986,7 @@ lamda.php lamejor lamer lamina.php +laminas laminat.html lamination lamm @@ -65700,6 +66330,7 @@ launch launch.aspx launch.html launch.php +launchd launchdarkly launcher launchpad @@ -65758,6 +66389,7 @@ lb-gb lb-monitoring.html lb.html lb.php +lb/config lbadmin lbadmin.php lbd.php @@ -66132,6 +66764,7 @@ letter.htm letter.html letter.php letter_opener +letter_opener_web letterhead letters letters.aspx @@ -66170,6 +66803,7 @@ lexus.html lf lfc/fixtures/superuser.xml lfc/fixturessuperuser.xml +lfi lfm.php lftp lg @@ -66312,6 +66946,7 @@ libraries libraries.asp libraries.aspx libraries.php +libraries/cms/version/version.php libraries/joomla/database libraries/joomla/version.php libraries/phpmailer @@ -66353,6 +66988,9 @@ libs libs.html libs.php libs/.env +libs/cookie-consent.js +libs/cookie.js +libs/cookies.js libs/granite/core/content/login.html libs/granite/core/content/login/favicon.ico libsecure.php @@ -66525,6 +67163,7 @@ lighting lightning lights lightspeed.php +lightstep/api/v0.1/reports lighttpd lighttpd.access.log lighttpd.error.log @@ -66566,6 +67205,9 @@ limited Limited limited, limits +limits/api +limits/current +limits/remaining limitTypes limo limpa.php @@ -66586,6 +67228,7 @@ line9 line_items linea.php linea_faq.jsp +linear/api/graphql linecontrol lines lines.aspx @@ -67080,6 +67723,7 @@ listen listen.php listener.log listener.php +listeners listeners?format=json listerpage.aspx listes @@ -67229,6 +67873,7 @@ live/.env.local live/.env.production live/.env.staging live/config.env +live/stream live_chat.html live_chat.php live_comments.php @@ -67264,6 +67909,8 @@ livehelp_step2.php livehelp_step3.php liveique_macros.vm livejournal +livekit +livekit/api livelife liveness livepages @@ -67326,6 +67973,8 @@ ljf ljgm.asp ljh.asp lk +lk2 +lk2/manifest_lk2.json lk.htm lk.php lk/login @@ -67373,6 +68022,7 @@ lnspiderguy lnvideos lo load +load-balancer/config load-scripts.php load-styles.php load.asp @@ -67574,6 +68224,7 @@ locus7shell.php locus7shell.phtml locus7shell.py locus.php +locust lodel lodges lodging @@ -67656,7 +68307,13 @@ log/.env log/access.dat log/access.log log/access_log +log/admin-access.bak +log/admin-access.old +log/admin-error.log +log/admin-error.txt log/admin.log +log/admin_access.log +log/admin_access.txt log/app.log log/audit.log log/auth.log @@ -67682,6 +68339,7 @@ log/payment.log log/payment_authorizenet.log log/payment_paypal_express.log log/production.log +log/report log/security.log log/server.log log/system.log @@ -67745,6 +68403,7 @@ logconfig.php logdata logdevelopment.dat logdevelopment.log +logdna logemann logerror.dat logerror.log @@ -67768,6 +68427,7 @@ LogfileSearch logfilesstorage LogfileTail logfileview +logflare/api/logs loggain.aspx logged loggedin @@ -67875,6 +68535,8 @@ login.php3 login.php5 login.php.bak login.php.old +login.php.orig +login.php.swp login.php~ login.phtml login.phyml @@ -67900,18 +68562,27 @@ login/admin/admin.asp login/adminadmin.asp login/administrator Login/adminlogin.aspx +login/apple Login/auth.php login/cpanel login/cpanel.asp login/cpanel.php +login/discord +login/facebook +login/github +login/google login/index login/index.php +login/linkedin login/login login/login.php +login/microsoft login/oauth Login/sign.php +login/slack login/super login/super.php +login/twitter login_ login_2.php login_@123 @@ -67989,6 +68660,7 @@ login_users login_usuario.html LoginAction.do loginadmin +loginadmin.php loginasp Loginauth.php loginautoset.asp @@ -68188,6 +68860,7 @@ logproduction.log logreferrer.php logreport logreports +logrocket logrono.html logs LOGS @@ -68327,11 +69000,13 @@ logsproxy_access_ssl_log logsproxy_error_log logstash logstash.yml +logstashadmin logstats logsurfer logsuser.log logswsadmin.traceout logswww-error.log +logtail logtest.log logtest.php logtmp @@ -68351,8 +69026,12 @@ lokales loki loki.php loki/api +loki/api/v1/label +loki/api/v1/label/pod/values loki/api/v1/labels +loki/api/v1/push loki/api/v1/query +loki/api/v1/query_range loki/api/v1/rules loki/api/v1/series loki/api/v1/tail @@ -68627,6 +69306,8 @@ lucasarts lucene lucent lucero +luci +luci-static luci.jsp.spy2009.jsp lucia lucian @@ -68664,6 +69345,7 @@ lulu lululemon lumb-entry.php lumbroso +lumen lumen.log lumigent luminox.php @@ -69024,6 +69706,7 @@ mac.php macadmin macallan macaroni1 +macaroon maccosmetics macedonia macedonia.html @@ -69522,6 +70205,7 @@ Main_Page main_page main_page.php main_result.php +main_service.php main_special.cfm main_user/.env MainActivity.php @@ -69602,6 +70286,11 @@ maintenance.html maintenance.php maintenance.shtml maintenance.txt +maintenance/disable +maintenance/enable +maintenance/off +maintenance/on +maintenance/status maintenance/test2.php maintenance/test.php maintenance_pages @@ -69969,6 +70658,9 @@ managelogin.php managemail.php managemake.aspx management +management-console.htm +management-console.php +management-console.phtml management.aspx management.htm management.html @@ -70119,6 +70811,9 @@ management/version management/webhooks management/withdrawals management/workers +management_console.htm +management_console.php +management_console.phtml managementconfigprops managementenv managepanel @@ -70306,6 +71001,7 @@ manager/subscriptions manager/system manager/tags manager/templates +manager/text manager/themes manager/tokens manager/trace @@ -70379,6 +71075,7 @@ manifest/cache manifest/logs manifest/tmp manila +maniphest manman mannheim manninen @@ -70451,6 +71148,7 @@ manufacturing.aspx manufaktur manulife manunited +manuscript.php manutencao manutencao.asp manutencao.html @@ -70605,6 +71303,7 @@ margaret margarida margie maria +mariadb mariage mariah mariajose @@ -70798,6 +71497,7 @@ master.tar.bz2 master.tar.gz master.zip master/.env +master/api master/login master/panel master/portquotes_new/admin.log @@ -70842,6 +71542,8 @@ masterRqmntsEN.php masters mastersfusion.br masthead +mastodon/api/v1/accounts/verify_credentials +mastodon/api/v1/instance mat mat880 mat.php @@ -70894,6 +71596,7 @@ matomo/index.php?module=API&method=API.getMatomoVersion matomo/login matriks matrikzgb +matrix matrix.js matrix.php matrix_engine @@ -70901,6 +71604,9 @@ matrox matsushita matt mattermost +mattermost/api/v4/system/ping +mattermost/api/v4/teams +mattermost/api/v4/users matthew matthews matthias @@ -70929,6 +71635,7 @@ max-templates/default/stylebbsmax_forum.css.fast.aspx max-wilhelm max.htm maxage +maxcdn MaxDepth.php maxdev maxheight.js @@ -71095,6 +71802,7 @@ meagan.asp meandyou measure measure.html +measurement measurements.pdf meaweb/os/mxperson meaweb/osmxperson @@ -71179,6 +71887,7 @@ mediakit mediakit.html mediakitnav.cfm mediamarkt +mediamtx medianamik mediapedia mediaplayer @@ -71189,9 +71898,13 @@ medias mediaserv mediasize.php mediaslash +mediasoup +mediasoup/api mediastore mediatext mediawiki +mediawiki/api.php +mediawiki/index.php medical medical.html medicare @@ -71317,6 +72030,7 @@ member.bak member.cgi member.htm member.html +member.inc member.php member/2fa member/about @@ -71804,6 +72518,7 @@ membres_dev membro membros memcache.php +memcache/flush memcache_test.php memcached memcached/.env @@ -72307,6 +73022,8 @@ mercuryboard mercurysteam meredith merge +merge-request +merge-requests merge.lib.php merge.php mergephrase.fil @@ -72580,6 +73297,7 @@ metric-api metric-apimetrics metric_tracking metric_tracking.json +metricbeat metrics metrics.json metrics.php @@ -72604,10 +73322,16 @@ mexico mexico-df.html mexico.htm mexico.html +mezmo mezuak +mezzio mf mfa +mfa/backup-codes mfa/challenge +mfa/disable +mfa/setup +mfa/verify mfg mfg.php mfgvsmodularhomes.x @@ -72620,7 +73344,9 @@ mfr_admin.php mfriend.php mfs mg +mg-admin mg-core +mg-plugins mg.html mg.jsp mg.php @@ -72766,6 +73492,7 @@ middleman middleware middleware.php middleware.php.bak +middlewares midget midhosting midi @@ -72817,6 +73544,7 @@ migrationinfo migrationlogin.php migrations migrations.php +migrations/versions migrationsauth.php migrationsign.php migrationsignin.php @@ -72845,6 +73573,7 @@ milano mildred milena miles +milestone milestones milestones.html Milestones.php @@ -72935,6 +73664,8 @@ mini4.php mini5.php mini-profiler-resources/flamegraph mini-profiler-resources/results +mini-profiler-resources/results-index +mini-profiler-resources/results-list mini.cgi mini.php mini.php5 @@ -72980,11 +73711,15 @@ minimum minina mining mininuke +minio minio/health/cluster minio/health/live minio/health/ready +minio/login minio/metrics/v3/cluster/health minio/metrics/v3/system/drive +minio/minio/health/cluster +minio/minio/login minions miniportail miniportal @@ -73089,6 +73824,8 @@ misha mishka misnotas.php misp +misp/attributes/restSearch +misp/events/index miss1.htm miss2.htm miss-video.com @@ -73159,13 +73896,17 @@ mix_entry.php mixed mixer mixes.php +mixpanel/engage +mixpanel/track mizuno mj +mjpg/video.mjpg mju.swf mk mk1hf63j.php mk.php mkdir +mkdocs mkdocs.yml Mkfile.old mkmf.log @@ -73973,6 +74714,7 @@ modular module module.aspx module.functions.php +module.md Module.php module.php Module.symvers @@ -74155,9 +74897,11 @@ moneycard.php moneyorder.php mongo mongo-backup +mongo-compass mongo-cons.php mongo-db/bak-files/backup.bak mongo-express +mongo-express/db mongo-test.php mongo.log mongo.php @@ -74166,6 +74910,7 @@ mongodb mongodb/config/dev/.env mongodump mongodump.archive +mongoexpress mongolia.html mongoose mongotest.php @@ -74413,6 +75158,7 @@ mountain mountain-works.php mouse mouse1 +mouseflow mouseover.js mouser mousikomi.html @@ -74646,6 +75392,7 @@ mss-shop.nsf mssccprj.scc msservice mssql +mssql-admin mssql.asp mssql.aspx mssql.jsp @@ -74693,6 +75440,7 @@ mt/mt-xmlrpc.cgi mt/mt.cgi mt_images mta +mta-sts.txt mtapi.php mtb100 mtc @@ -75177,6 +75925,7 @@ mydomainadministrator.php mydomainlogin.php mydownload.php mydownloads +mydumper myebay myEditor myenv.php @@ -75608,8 +76357,10 @@ nacos/v1/auth/users?pageNo=1&pageSize=9 nacos/v1/auth/users?pageNo=1&pageSize=10 nacos/v1/console/namespaces nacos/v1/console/server/state +nacos/v1/cs/configs nacos/v1/cs/configs?dataId=&group=&tenant=&search=accurate nacos/v1/cs/configs?search=blur&dataId=&group=&tenant= +nacos/v1/ns/instance nacos/v1/ns/operator/servers nacos/v1/ns/service/list?pageNo=1&pageSize=100 nacos/v2/cs/config?dataId=&group=&namespaceId= @@ -75632,6 +76383,7 @@ nagios/cgi-bin/status.cgi nagios/cgi-binstatus.cgi nagios_admin nagios_get.php +nagiosadmin nagiosxi nagl nahicodeofethics @@ -75730,6 +76482,7 @@ native_stderr.log native_stdout.log nativeEncoding.txt nats +natsadmin natterchat natural.htm nature @@ -75947,6 +76700,9 @@ nemo nenalinda neneng neo +neo4j +neo4j/browser +neo4j/db/neo4j/tx neoboard neocrome neomail @@ -76094,6 +76850,7 @@ netserver netsoft netsoltrademark.php netsolutions +netsparker netspell netstat netstats @@ -76695,6 +77452,7 @@ newsign.aspx newsignup.php newsimage newsimages +newsindex.php newsinfo newsinsert2000.pdf newsite @@ -76880,6 +77638,8 @@ nextstep nextstep.aspx nextweb nexus +nexus/repository +nexusadmin nf nf2004text nf.aspx @@ -76920,6 +77680,8 @@ nginx.conf nginx.conf.sample nginx.htaccess nginx/.env +nginx/cache/clear +nginx/cache/purge nginx/status nginx_access.log nginx_access.log.1 @@ -77044,6 +77806,7 @@ nikto nikung.php nikwax.php nilsen +nimble nimcache nimda nimrod @@ -77241,6 +78004,7 @@ node_modules node_modules/.env node_stats nodeadd +nodeinfo Nodejs-Projects/play-ground/login/.env Nodejs-Projects/play-ground/ManageUserRoles/.env nodes @@ -77303,6 +78067,7 @@ nomatch.aspx nombas nombre nombres +nomer.php nominate_topic.php nominations noms @@ -77546,12 +78311,14 @@ notifyme.action notifyme.asp notifyme.cfm notimportant +notion notizia.php notizie notizie.php notjustbrowsing notloggedin.htm notmodrewrite +notpaid.php notrack.html notre-equipe.htm notregister.aspx @@ -77638,6 +78405,7 @@ npds nph-index.cgi nph-proxy.cgi nphp +npm npm-debug.log npm-shrinkwrap.json npm.json @@ -77719,6 +78487,7 @@ ntc ntdaddy.asp ntdaddy_v1.9.php NTDaddy_v1.9.php +ntfy ntlm_sasl_client.php ntlmaps ntmail @@ -77785,6 +78554,8 @@ num num.php number number9 +number.js +number.php Number.php Numberformat.php numberFormat.php @@ -77949,14 +78720,22 @@ oauth/admin oauth/authorization oauth/authorize oauth/callback +oauth/check_token +oauth/confirm_access oauth/device/code oauth/enter +oauth/facebook +oauth/github +oauth/google oauth/introspect +oauth/linkedin oauth/login +oauth/microsoft oauth/revoke oauth/signin oauth/token oauth/token/info +oauth/twitter oauth/userinfo oauth_secret.txt ob @@ -77964,6 +78743,7 @@ ob.lib.php obb_profiles obdc obefacade.aspx +obekts.php obelix75 oberhausen.html oberon2.php @@ -78064,6 +78844,10 @@ ocp.php ocportal ocr ocs +ocs/v1.php/apps +ocs/v1.php/cloud/groups +ocs/v1.php/cloud/users +ocs/v2.php/cloud/users ocsp ocsplocalhost oct06-sp.php @@ -78075,6 +78859,7 @@ oculto ocz6jsqm.php od oda.php +odata odbc odbc.aspx odbc.js @@ -78441,6 +79226,9 @@ oklahomacity okokok okqq okscripts +okta +okta/callback +okta/login oktoplay.pdf okwy_A_D_server.php okzhang1314 @@ -78642,6 +79430,7 @@ oms/admin oms_track omsk on +on-call on-line.html on.asp on_line.php @@ -78650,6 +79439,7 @@ ona onairsamdonaldson onboarding onbound +oncall oncology oncology.jsf ondemand @@ -78673,6 +79463,7 @@ oneadmin.phtml onecenter onecore onedotoh +onelogin oneorzero onepage onepiece @@ -78745,6 +79536,7 @@ onlineview.php only onlycern onlyme +onlyoffice onlyscript.info onmap.php onomisfotos @@ -78758,6 +79550,7 @@ ontario onthisday onuninstall.php onupdate.php +onvif onyx oo ooapp @@ -78857,6 +79650,8 @@ opencms openconf openconnect OpenCover +opencti +opencti/graphql opendb opendir opendir.php @@ -78871,6 +79666,8 @@ opener openfaq openfile openfind +openfire +openfire/login openforcead openftpd opengfs @@ -78882,6 +79679,9 @@ OpenID openid OpenID.php openid.php +openid/azure +openid/google +openid/okta openidcheck.php openinviter openjournal @@ -78897,6 +79697,7 @@ openobex openoffice OPENORDER.php openpgp +openpgpkey openphpnuke openpic.php openpkg @@ -78928,6 +79729,9 @@ openssl openstack openswan opentelemetry +opentelemetry/v1/logs +opentelemetry/v1/metrics +opentelemetry/v1/traces opentext opentime_bten2.php opentime_youku.php @@ -78939,6 +79743,7 @@ openui.log openurl openurl.asp openvas +openvas-scanner openview openvmps openvpn @@ -78982,6 +79787,9 @@ operation.htm operations operations.html operator +operator-console.htm +operator-console.php +operator-console.phtml operator.htm operator.php operator.phtml @@ -79126,6 +79934,8 @@ operator/version operator/webhooks operator/withdrawals operator/workers +operator_console.htm +operator_console.phtml operatori operators operators.html @@ -79306,7 +80116,10 @@ opros.html ops ops/admin ops/dashboard +ops/health +ops/metrics ops/vagrant/.env +opsgenie/api/v2/alerts opslag.php opsware opt @@ -79386,6 +80199,7 @@ oracl oracle oracle8.php oracle11.php +oracle-admin oracle.aspx oracle.jsp oracle.php @@ -79394,6 +80208,7 @@ oraclerewerewrew.php oracle脱裤脚本.jsp oradata oradb +oradmin oral.asp oral.pdf orange @@ -79963,9 +80778,13 @@ osog.php osp ospfd.conf osprey.php +osquery +osquery/api oss oss.html oss.php +ossec +ossec/logs ossim ossp osstoreadmin.php @@ -80019,6 +80838,8 @@ others.php others_chart.html othersbegin.asp othersites +otlp/v1/metrics +otlp/v1/traces oto oto1.html oto2.html @@ -80229,6 +81050,8 @@ own owncloud owncloud/.env owncloud/config +owncloud/index.php/login +owncloud/status.php owned owner owner.html @@ -80684,17 +81507,23 @@ packages.aspx packages.htm packages.html packages.php +packages/admin packages/api/.env packages/app/.env +packages/auth packages/client/.env packages/frontend/.env +packages/maven +packages/npm packages/plugin-analytics/src/fixtures/analytics-ga-key/.env packages/plugin-qiankun/examples/app1/.env packages/plugin-qiankun/examples/app2/.env packages/plugin-qiankun/examples/app3/.env packages/plugin-qiankun/examples/master/.env +packages/pypi packages/react-scripts/fixtures/kitchensink/template/.env packages/styled-ui-docs/.env +packages/user packages/web/.env packages_display.asp?ref= packagetrack.php @@ -80711,6 +81540,7 @@ packer_cache packers packet Packet.php +packetbeat packeteer packetpro.html packets @@ -80986,6 +81816,7 @@ Pager_Wrapper.php pagerank pagerank.php pagerduty +pagerduty/api/v2/incidents pagers pagerTest.php pages @@ -81465,6 +82296,7 @@ paperdemo_bill1.jsp papers papers.php paperthin +papertrail papichulo papigator papirkurv @@ -81589,6 +82421,7 @@ particle particulier partidos_pnvea.nsf parties +partition Partition.class.php partner partner1.html @@ -81953,6 +82786,7 @@ pasadena pascal pascal.php pascalau +paseto pasired.php pasmail.html paso @@ -82008,6 +82842,8 @@ passion passions passive passkey +passkey/authenticate +passkey/register passlist passlist.csv passlist.dat @@ -82269,6 +83105,8 @@ payline paylinki.mvc paylinkp.mvc payload +payload-admin +payloads paymanager payment payment2 @@ -82652,6 +83490,7 @@ peek peekaboo peel peel.php +peer peer.php peercast peerftp_5 @@ -82698,6 +83537,7 @@ penny pensacola pentaho pentax-store.html +pentest pentium penya penza @@ -82932,6 +83772,7 @@ pfs.php pftpl pfx pg +pg-admin pg.php pg_connect.php pg_customcode.asp @@ -82940,6 +83781,8 @@ pg_hba.conf pg_setup.asp pgadmin pgadmin4 +pgadmin4/browser +pgadmin4/login pgadmin.log pgadmin.php pgadmin/login @@ -82991,9 +83834,12 @@ pgm-tracking.php pgm-view_video.php pgosd pgp +pgp-key.txt +pgp/key pgpi pgpmail pgrefresh +pgrst pgs pgsql pgtId @@ -83003,6 +83849,9 @@ ph PH5P.php ph.php ph_vayv.php +phabricator +phabricator/login +phalcon phanmem phantasma.php PHANTASMA.php @@ -83511,6 +84360,7 @@ phpcommunitycalendar phpcompat.php phpconnect.php phpcounter +phpcs.xml phpdata.php phpdb phpdbadm.php @@ -84030,6 +84880,7 @@ phpraid phprank phprecipebook phpRedisAdmin +phpredisadmin phpredmin phpremoteview.php PHPRemoteView.php @@ -84380,8 +85231,10 @@ pingback pingback.php pingconnection.php pinger.php +pingfederate pinglun pinglun.asp +pingone pingpong pingserver.php pingshen @@ -84422,6 +85275,8 @@ pirates pirates.php Pirates.php pirch +pirsch/event +pirsch/hit pisa.html pisces pisces-horoscope @@ -84452,6 +85307,7 @@ pixel pixel2.php pixel.asp pixel.php +pixel.png pixelbender.php pixelpost pixels @@ -84491,6 +85347,7 @@ pkginfo.aspx pkginfo.php pkgs pki +pki-validation pkinc pkr pkware @@ -84621,7 +85478,9 @@ platypus.php platz_login platz_login.php Platzauswahl.php +plausible/api plausible/api/v1/stats +plausible/event plaxo_cb.html Play play @@ -84839,6 +85698,7 @@ pluto plx plymouth pm +pm2/list pm2_5_2.php pm2_5_3.php pm2_5_4.php @@ -84988,6 +85848,7 @@ pocasi.asp pocet.php pochta2.html pocket +pocketbase pocketpc poczta pod @@ -85135,6 +85996,7 @@ poll_success.php poll_thankyou.html poll_user poll_vote.php +polladmin pollbooth pollbooth.asp pollbooth.html @@ -85432,12 +86294,20 @@ portail portail_site.php portailphp portainer +portainer/api +portainer/api/containers/json portainer/api/endpoints +portainer/api/endpoints/1/docker/containers/json +portainer/api/images/json +portainer/api/networks portainer/api/registries +portainer/api/roles portainer/api/settings portainer/api/stacks portainer/api/status +portainer/api/teams portainer/api/users +portainer/api/volumes portainer/login Portal portal @@ -85876,6 +86746,8 @@ postmail postmail.html postmessage.aspx postmessage.php +postmortem +postmortems postnewad2.aspx postnuke postnukehtml @@ -87010,6 +87882,7 @@ privato privatschutz.htm prive privelink.asp +privilege-escalation PRIVILEGES privileges privkey.pem @@ -88158,10 +89031,14 @@ prom.html promanager promethe prometheus +prometheus/api/v1/alertmanagers prometheus/api/v1/alerts prometheus/api/v1/label/__name__/values +prometheus/api/v1/labels prometheus/api/v1/query prometheus/api/v1/rules +prometheus/api/v1/series +prometheus/api/v1/status/buildinfo prometheus/api/v1/status/config prometheus/api/v1/status/flags prometheus/api/v1/status/runtimeinfo @@ -88285,6 +89162,7 @@ propupdate prorat pros pros.php +prosody prosoft prospect prospect.asp @@ -88305,12 +89183,15 @@ protect.php protected protected/.env protected/config/console.php +protected/config/database.php protected/config/main.php protected/config/test.php protected/controllers +protected/controllers/SiteController.php protected/data protected/data/schema.mysql.sql protected/models +protected/models/User.php protected/runtime protected/views protected/yiic @@ -88331,6 +89212,7 @@ protel protetor proto proto.php +protobuf protocol proton prototipos @@ -88422,6 +89304,7 @@ proxy.php proxy.stream proxy.txt proxy.xml +proxy/config proxy_config.json proxy_error_bkrs2.php proxy_service.php @@ -88432,6 +89315,7 @@ proxyheader.php proxyport ProxyPriceInfo proxypwd +proxysql proxytest.jsp proxytui proxytunnel @@ -88658,6 +89542,7 @@ public/assets public/assets/.protected/.env public/config.js public/files +public/fonts public/hot public/index.php public/login @@ -88849,6 +89734,8 @@ puhovoi.php pui_link.html pujar pull +pull-request +pull-requests pullover.aspx pulse pulse/dashboard @@ -88864,6 +89751,7 @@ punbb115.inc.php punbb_users punchout punctweb +pundit puneet punetoret punish.php @@ -88910,6 +89798,7 @@ purepostpro purgatory purge purge.php +purge/cache PurifierLinkify.php purity purple @@ -88934,6 +89823,7 @@ pushlivecurl2.php pushlivecurl.log pushlivecurl.php pushlogs.log +pushover pushtestcurl.php pussy put @@ -89011,8 +89901,11 @@ py-compile py-livredor py-membres pyblosxom +pydio +pydio/index.php pydo pyj_artikutza.nsf +pypi pyproject.toml pyramid pyramid.htm @@ -89245,6 +90138,8 @@ quantum quantumsuccess.asp quarantine quarterly +quartz +quartz/jobs qub que que.php @@ -89285,6 +90180,7 @@ query.jsp query.log query.php query?q=SHOW+DATABASES +query_info.php querydong.jsp QUERYHIT.HTM queryhit.htm @@ -89331,8 +90227,11 @@ Queue queue queue.php queue.php.bak +queue/admin queue/email +queue/list queue/mail +queue/status queue_worker.php queues quezza @@ -89454,6 +90353,7 @@ quizzes quota quota.cgi quota.php +quota/status quotas quotation quotation.php @@ -89707,6 +90607,7 @@ rails-api/react-app/.env rails.php rails/actions rails/actions?error=ActiveRecord +rails/conductor rails/db rails/info rails/info/properties @@ -89715,6 +90616,7 @@ rails/info/routes rails/info/routes.json rails/infoproperties rails/mailers +rails_admin railsactions railsapp/config/storage.yml railway @@ -89757,6 +90659,7 @@ ramsite ran ranch rancher +rancher/v3/clusters rancid rand rand.php @@ -89855,6 +90758,8 @@ rate2.php rate-details.aspx rate-game rate-it.php +rate-limit +rate-limits rate-product.aspx rate-this rate-this-item @@ -89899,6 +90804,7 @@ rateit.asp rateit.aspx rateit.cgi rateit.php +ratelimit ratelink.php ratemypic ratenews.php @@ -89999,6 +90905,7 @@ rca rcart.asp rcblog rcc +rce rcf rcheckout.php rci.ashx @@ -90020,6 +90927,8 @@ rcjakar/adminlogin.js Rcjakar/adminlogin.php rcjakar/adminlogin.php rcLogin +rclone +rclone/api rcm rcms rcom.php @@ -90073,6 +90982,7 @@ reach.cfm reach/sip.svc reachsip.svc react +react-dom-client.production.js react_todo/.env reacties reaction @@ -90087,6 +90997,7 @@ reactivepower3.php Read read Read%20Me.txt +read-only read.asp Read.aspx read.aspx @@ -90170,8 +91081,10 @@ readmessage.cfm readmore.php readnews.asp readnews.php +readonly readpmsg.php readreviews.aspx +readthedocs readwx.cfm ready ready4xmas.html @@ -90468,6 +91381,7 @@ recoverpassword.jsp RecoverPassword.php recoverpassword.php recovery +recovery-codes recovery.aspx recovery.html recovery.php @@ -90671,10 +91585,12 @@ redis.log redis.php redis.txt redis/admin +redis/flush redis_credentials.txt redisadm redisadmin redisinsight +redisinsight/api redkernel redman redmine @@ -90766,6 +91682,7 @@ referral.htm referral.html referral.jsp referral.php +referral/track referral_add_set.php referral_asign.php referral_request.php @@ -91093,6 +92010,7 @@ rei reiki.html reimbursements reimg +reindex reindex_search.cfm reinstall reise @@ -91187,6 +92105,8 @@ releases.html releases.php releases.txt releases.xml +releases/latest +releases/tag releases_headlines_details.asp?id= relexample_project religion @@ -91265,6 +92185,11 @@ remote.htm remote.html remote.php remote.php/dav +remote.php/dav/addressbooks +remote.php/dav/calendars +remote.php/dav/files +remote.php/ocs/v2.php/apps/files +remote.php/webdav remote/fgt_lang?lang=/../../../../////////////////////////bin/sslvpnd remote/fgt_lang?lang=/../../../../////////////////////////binsslvpnd remote/fgt_lang?lang=/../../../..//////////dev/cmdb/sslvpn_websession @@ -91427,6 +92352,7 @@ replacement.php replacephotos.php replay replica +replica/api replicas replicate replicated @@ -91469,6 +92395,7 @@ report-detail.asp?id= report-error report-paper.php report-spam.html +report-uri report.7z report.asp report.aspx @@ -91522,6 +92449,8 @@ reportgame.php reportid reporting reporting/admin +reporting/v1/csp +reporting/v1/hpkp reportlisting.php reportlocation.aspx reportproduct.aspx @@ -91824,6 +92753,8 @@ residential residential-roofing.php residential.asp residents +resilience +resilience4j resim resim.php resimler @@ -91924,8 +92855,14 @@ resources/docker/phpmyadmin/.env resources/docker/rabbitmq/.env resources/docker/rediscommander/.env resources/fckeditor +resources/lang/en/auth.php +resources/lang/en/messages.php +resources/lang/en/passwords.php +resources/lang/en/validation.php +resources/lang/ru/messages.php resources/sass/.sass-cache resources/tmp +resources/translations/en.yaml resources/views resources/views/admin/dashboard.blade.php resources/vulnerabilities_list.asp?id= @@ -91966,6 +92903,10 @@ resposta.html respre.cgi respuesta.php resque +resque/failed +resque/overview +resque/queues +resque/workers resserver.php ressource ressources @@ -92039,6 +92980,7 @@ restaurantinfo.aspx restaurantmenu.aspx restaurants restaurants.php +restic restituda restock restore @@ -92163,6 +93105,7 @@ retard retired retirement retirement.htm +retool/api retorno.php retour.php retourzenden.php @@ -92618,6 +93561,7 @@ robots-old.txt robots.php robots.phtml robots.txt +robots.txt.dist robots/.env robots_ssl.txt robotstats @@ -92641,6 +93585,8 @@ rocketman rockets rockliffe rockme +rockmongo +rockmongo/index.php rocknroll rocks rocku @@ -92684,10 +93630,14 @@ rolex ROLIK_OUT.php roll rollback +rollbar/deploy +rollbar/item roller roller.html rollingstone rollingStone +rollout +rollout/status rollover.js rolltext rolltide @@ -93017,6 +93967,7 @@ routes.go routes.htm routes.php routes.php.bak +routes.rb routes/.env routes/api.php routes/error_log @@ -93057,6 +94008,7 @@ rpc.asp rpc.html RPC.php rpc.php +rpc/api rpc_admin rpc_admin.php rpc_relay.html @@ -93270,6 +94222,8 @@ rt_styleswitcher.php rt_utils.php rta rtasarim +rtc +rtc/api rte rte-snippets RTE_configuration @@ -93282,11 +94236,14 @@ rti rtl rtm rtm.log +rtmp rtn_login08.php rtn_login.php rtndjy3s.php rto rtr +rtsp +rtsp-simple-server rtsp.php rtv ru @@ -93366,6 +94323,8 @@ run.sh run.zip run/.env run_1.js +runbook +runbooks runcms runcrawl.php runcronjobs.php @@ -93501,7 +94460,9 @@ s2tu6m70.php s3 s3.yml s3/admin +s3/login s3_credentials.txt +s3admin s3c.php s3cmd.ini s3proxy.conf @@ -93773,10 +94734,15 @@ samhain samira saml saml/acs +saml/azure saml/callback +saml/google +saml/jumpcloud saml/login saml/logout saml/metadata +saml/okta +saml/onelogin saml/sls samlauth2.php samlauth.php @@ -93894,6 +94860,7 @@ sandy sane sanfran sanfrancisco +sanic sanita sanitize.php sanitizer @@ -94226,6 +95193,7 @@ scalix scalyr scamper scan +scan-results scan.aspx scan.php scan.phtml @@ -94312,6 +95280,7 @@ schema.ser schema.sql Schema.sql schema.sql.gz +schema.yaml schema.yml schema_dump.sql schemas @@ -94535,7 +95504,10 @@ Scripts.xml scripts.zip scripts/.env scripts/.env.js +scripts/admin scripts/app/components +scripts/backup +scripts/build scripts/cgimail.exe scripts/check-package-versions.mjs scripts/ckeditor/ckfinder/core/connector/asp/connector.asp @@ -94544,9 +95516,12 @@ scripts/ckeditor/ckfinder/core/connector/aspx/connector.aspx scripts/ckeditor/ckfinder/core/connector/aspxconnector.aspx scripts/ckeditor/ckfinder/core/connector/php/connector.php scripts/ckeditor/ckfinder/core/connector/phpconnector.php +scripts/cleanup scripts/components scripts/convert.bas scripts/counter.exe +scripts/cron +scripts/deploy scripts/fckeditor/editor/filemanager scripts/fckeditor/editor/filemanager/browser/defaultbrowser.html scripts/fckeditor/editorfckdialog.html @@ -94566,6 +95541,7 @@ scripts/link.js scripts/make-changelog scripts/make-release scripts/manage_translations.py +scripts/migrate scripts/no-such-file.pl scripts/open-api/public/index.html scripts/open-api/serve.js @@ -94581,7 +95557,9 @@ scripts/rpm-install.sh scripts/samples scripts/samples/search/webhits.exe scripts/samples/searchwebhits.exe +scripts/seed scripts/setup.php +scripts/test scripts/tiny_mce scripts/tiny_mce/plugins/ajaxfilemanagerajax_login.php scripts/tiny_mce/plugins/ajaxfilemanagerajaxfilemanager.php @@ -94677,10 +95655,12 @@ se.secure sea sea-to-summit.php sea.phtml +seafile seafile/api2 seagate seaglass seagull +seahub seal.php sealInvoice.php sealInvoice_recuperado.php @@ -94689,6 +95669,7 @@ sealInvoiceRespaldo3Nov.php sealInvoiceRespaldo151117.php sealRetention.php sealskinz.php +seam-admin sean seanox seanpaul @@ -94877,6 +95858,7 @@ search_print.php search_prod.html search_products.htm search_products.php +search_queries.php search_query.aspx search_quick.asp search_res.php @@ -95150,6 +96132,7 @@ secret.php secret.txt secret.yaml secret/admin +secret/config secret/index.php secret/login secret_admin @@ -95455,12 +96438,17 @@ security.txt security.xml security.yml security/config +security/csp-report +security/ct-report +security/hpkp-report Security/login security/policies +security/report security/roles security/rules security/user/authenticate security/users +security/xss-report security_banip.php security_image.php security_images @@ -95501,6 +96489,9 @@ sefaz sefl sefl.old seger.php +segment +segment/v1/page +segment/v1/track segments segnala-abuso segnala.asp @@ -96262,6 +97253,10 @@ sentraweb sentry sentry/api/0/organizations sentry/api/0/projects +sentry/api/0/store +sentry/api/store +sentry/envelope +sentry/store seo seo-blog seo-board @@ -96412,16 +97407,19 @@ server/.env.local server/.env.production server/.env.staging server/admin +server/config server/config.json server/config/.env server/laravel/.env server/login +server/metrics server/reboot server/restart server/server.js server/src/persistence/.env server/status server/storage +server/version server_6.php server_7.php server_8.php @@ -96533,6 +97531,8 @@ service.php service.php.bak service.php~ service.pwd +service.wsdl +Service.wsdl service.yaml service/.env service/.env.local @@ -96550,6 +97550,7 @@ service/enter service/enter.php service/health service/info +service/list service/login service/login.php service/login.phtml @@ -96557,6 +97558,7 @@ service/metrics service/metrics/data service/metrics/healthcheck service/ping +service/reload service/rest/swagger.json service/rest/v1/assets service/rest/v1/blobstores @@ -96602,6 +97604,7 @@ serviceinterface servicelist servicelogin.php servicelogin.phtml +servicenow/api/now servicerequest.php servicerfp services @@ -96618,6 +97621,8 @@ services.html services.php services.php_files services.tar.gz +services.wsdl +Services.wsdl services/.env services/.env.local services/.env.production @@ -96631,11 +97636,13 @@ services/graylog/.env services/health services/help services/jaeger/.env +services/list services/minio/.env services/monitoring/.env services/portainer/.env services/redis-commander/.env services/registry/.env +services/reload services/simcore/.env services/status services/traefik/.env @@ -96766,6 +97773,7 @@ session session35.php session44.php session63.php +session-replay session-update.ashx session.asp session.inc.php @@ -96780,6 +97788,12 @@ session.madeline.safe.php.lock Session.php session.php session.xml +session/create +session/destroy +session/info +session/refresh +session/status +session/validate session_expired.jsp session_member_id session_member_login_key @@ -96813,7 +97827,11 @@ sessions.php sessions.sql sessions.xml sessions/.env +sessions/active +sessions/all +sessions/invalidate sessions/new +sessions/revoke SessionServlet sessionsnew sessionstate.aspx @@ -97160,6 +98178,7 @@ shannon shanti shape sharc +shard share share1.php share42 @@ -97279,6 +98298,7 @@ shell.phtml shell.py shell.sh shell.txt +shell/run shell_6.php shell_7.php shell_8.php @@ -98156,6 +99176,12 @@ sidebarGenerator.php sidebars sidecart.asp sidekiq +sidekiq/busy +sidekiq/dead +sidekiq/morgue +sidekiq/queues +sidekiq/retries +sidekiq/scheduled sidekiq_monitor sidemenu.cfm sidemenu.php @@ -98167,6 +99193,8 @@ sidney sidx siebel siegel +siem +siem/dashboard siemens siempre siena.html @@ -98212,9 +99240,13 @@ sign_up sign_up.html sign_up.php signage +signal signal.html signaler.html signaler.php +signaling +SignalR +signalr signalr/hubs signalr/negotiate Signature @@ -98516,6 +99548,10 @@ site72 site2010 site-admin site-admin.php +site-admin/auth +site-admin/login +site-admin/logs +site-admin/signin site-config site-contact.html site-help.html @@ -98763,6 +99799,7 @@ sitefeedback.aspx sitefiles sitefinity sitefinity/login +sitefinitywebservices siteforum siteframe sitegen @@ -98828,6 +99865,7 @@ sitemap-dev.xml sitemap-develop.xml sitemap-en.html sitemap-groups0.xml +sitemap-images.txt sitemap-index.xml sitemap-old.jsp sitemap-prod.xml @@ -98860,8 +99898,13 @@ sitemap.xml sitemap.xml.gz sitemap.xml.old sitemap.xml.php +sitemap/images.xml +sitemap/news.xml +sitemap/video.xml sitemap_0_5000.html +sitemap_1.xml sitemap_1.xml.gz +sitemap_2.xml sitemap_2.xml.gz sitemap_3.xml.gz sitemap_4.xml.gz @@ -98924,6 +99967,8 @@ siterefer siteroot.php sites Sites +sites-available/default +sites-enabled/default sites.asp sites.aspx sites.cfm @@ -99184,6 +100229,8 @@ skype.htm skyscanner skystream skywalker +skywalking +skywalking/graphql sl sl.html sl.php @@ -99192,6 +100239,8 @@ sla sla.html slabel slabel.php +slack/api +slack/api/auth.test slack_token.txt slacker slackware @@ -99200,11 +100249,13 @@ slamdunk slanadmin slanadmin.php slapd.conf +slas slashcode slashdot slashem slava slave +slave/api slax slayer sldsystem @@ -99258,6 +100309,7 @@ Sliding.php sliding_contact.php sliding_contact.php~ slike +slim slim4life.html slim10.html slimbox @@ -99360,6 +100412,7 @@ smartisoft smartline smartlink.js smartlist +smartlook smartmax smartoptimizer smartoptimizer/minifierscss.php @@ -99523,12 +100576,14 @@ snapshot snapshot.php snapshot.sql snapshot.tar.gz +SnapshotJPEG snapshots snapstream snatch snatch.php snd sne.php +sneakers sneakysneakysnake sneezy snert @@ -99547,6 +100602,7 @@ SnIpEr_SA.php sniper_sa_shell.php snippet snippet.ashx +snippet.md snippetmaster snippets snips @@ -99603,6 +100659,8 @@ soanimesitehd2 soap soap.htm soap.php +soap/api.wsdl +soap/service.wsdl soap/servlet/soaprouter soap/servlet/Spy soap_server_usuarios_registrados_java.php @@ -99624,6 +100682,7 @@ sobreRDT.php soc soc.html soc/admin +soc/dashboard soc_alterar_f.php soc_cep_f.php soc_cidade_f.php @@ -99639,6 +100698,11 @@ social-security.asp social.htm social.html social.php +social/facebook +social/github +social/google +social/linkedin +social/twitter social_catalogo.nsf social_centros.nsf social_datos.nsf @@ -99843,6 +100907,7 @@ sophos soporte soporte.html soqor +sorcery sord sorder sorenson @@ -100246,6 +101311,7 @@ sperre.php spexec.aspx spezial spf +SPF spgpartenaires sphera sphider @@ -100328,6 +101394,8 @@ sploit splunk splunk/apps splunk/services +splunk/services/collector +splunk/services/collector/event spn.html spo spock @@ -100404,6 +101472,8 @@ spread-betting spread.php Spreadsheets spring +spring-mvc +spring-security-oauth-resource spring-security-rest spring.log spring.log.1 @@ -100662,6 +101732,7 @@ src/administrator.php src/app.js src/assembly/.env src/auth.php +src/bootstrap/app.php src/character-service/.env src/client/mobile/.env src/Controller @@ -100809,8 +101880,14 @@ ssmtp ssn ssn.html sso +sso/azure sso/callback +sso/github +sso/google +sso/jumpcloud sso/metadata +sso/okta +sso/onelogin sso/saml sso_config.php ssodad @@ -100826,11 +101903,13 @@ sspv.xml sspwiz Ssr1986 ssr.php +ssrf SSS sss sss.php ssssss sst +ssti ssu ssv3_directory.php sswadmin @@ -100863,6 +101942,7 @@ stack stackato-pkg/.env stackdump stackguard +stackpath stackshield stackstorm stacktrace.log @@ -100879,6 +101959,9 @@ stadtplan-d.html stadtteilplaene staff staff-area +staff-console.htm +staff-console.php +staff-console.phtml staff-list.php staff-login.php staff.asp @@ -101035,6 +102118,8 @@ staff/workers staff_area staff_buttons.php staff_buttonsEN.php +staff_console.htm +staff_console.phtml staff_directory staff_directory.cfm staff_display.cfm @@ -101084,6 +102169,7 @@ stalkerlab stalled_issues.php stallion stallions +stamember.php stamp-h1 stampa stampa.asp @@ -101131,6 +102217,7 @@ starforce stargate stargirl starhub +starlette starlight starphire stars @@ -101295,6 +102382,7 @@ static/apiswagger.yaml static/cdr-stats/jsjquery static/dump.sql static/emq.ico +static/fonts static/javascriptcommon.js static/jsadmincp.js static/jsbeyond.js @@ -101856,6 +102944,7 @@ stratford stratus stratus.php strawber +strawberry strcasecmp.php strcspn.php streaks.asp @@ -101864,6 +102953,8 @@ stream.asp stream.asx stream.html stream.php +stream/hls +stream/live stream_actions.php stream_file.aspx stream_image.aspx @@ -102037,6 +103128,7 @@ stuff.txt stuffed stuffedwhugslp.cfm stumbleinside +stun stunnel stupid sturgeon @@ -102521,6 +103613,7 @@ summer.html summerschool summertime summit +sumologic sumthin sumus sun @@ -103045,6 +104138,7 @@ surgemail/mtemp/surgeweb/tpl/shared/modulesswfupload.swf surgemail/mtemp/surgeweb/tpl/shared/modulesswfupload_f9.swf surgeons surgery +suricata suriname.html surname surnames @@ -103151,6 +104245,7 @@ SVN.xml svn/all-wcprops svn/entries svn/format +svn/repos svn/wc.db svnserve.conf svr @@ -103164,6 +104259,8 @@ sw_sm_sw4.php swagger swagger-json swagger-resources +swagger-resources/configuration/security +swagger-resources/configuration/ui swagger-resources/configurationui swagger-resources/restservices/v2/api-docs swagger-ui @@ -103211,6 +104308,7 @@ swagger/v2swagger.yaml swagger/v3.0/api-docs swagger/v3.0/swagger.json swagger/v3.0/swagger.yaml +swagger/v3/api-docs swaggerapi swaggerapi-docs swaggerindex.html @@ -103415,6 +104513,8 @@ synchronizetagsfrom syncNode.log syncro syncsort +syncthing +syncthing/rest syndicate syndicate-list.asp syndicate.asp @@ -103454,8 +104554,14 @@ sys.html sys.jsp sys.php sys/admin +sys/config +sys/health +sys/info sys/login +sys/metrics sys/pprof +sys/status +sys/version sys_admin sys_admin.php sys_error.log @@ -103685,7 +104791,10 @@ system43.php system51.php system81.php system-admin +system-admin.htm system-admin.php +system-admin.phtml +system-admin.txt system-administration system-administration.php system-config/.env @@ -103740,12 +104849,14 @@ system/cms system/comments system/compliance system/config +system/config/default.php system/configdefault.php system/console system/console/bundles system/console/configMgr system/console/status-Bundlelist.txt system/content +system/core system/core/CodeIgniter.php system/cron system/cron/cron.txt @@ -103914,6 +105025,7 @@ systembackups.rar systembackups.tar.gz systembackups.zip systemchart.php +systemd systeme SystemErr.log systemerror.asp @@ -103937,6 +105049,7 @@ systems Systems systems. systems.php +systems/admin.js systems/login SystemService.asmx systemsoft @@ -103946,6 +105059,7 @@ systest.php systime sysuser sysusers +sysvinit sz szab-test szabalyzat.php @@ -104093,6 +105207,7 @@ tableId TableList tableName tablename +tableplus tableprefix tables tables2.htm @@ -104325,6 +105440,7 @@ tarragon tars tarsalgo tartarus +tartiflette tarzan tas tasha @@ -104338,7 +105454,10 @@ taskid tasklogs.en.txt tasks tasks.inc.php +tasks/admin +tasks/list tasks/main.yml +tasks/status tassel-confirm.php taste tatham @@ -104545,6 +105664,7 @@ teams.php teamshare teamstudio teamware +teamwork teapop tearepair.php teaser @@ -104754,6 +105874,9 @@ telefon.php telefonia telefonica telefono +telegraf +telegram/api +telegram/bot telekom telekorn telephone @@ -105168,6 +106291,7 @@ templates/defaultinfo.txt templates/index.html templates/ja-helio-farsi/index.php templates/ja-helio-farsiindex.php +templates/javascripts.js templates/protostar templates/rhuk_milkyway templates/rhuk_milkyway/0day.php @@ -105997,6 +107121,12 @@ tests.jsf tests.php tests/.env Tests/Application/.env +tests/database/manager_users.sql +tests/database/site_content.sql +tests/database/site_templates.sql +tests/database/site_tmplvar_templates.sql +tests/database/site_tmplvars.sql +tests/database/user_attributes.sql tests/default_settings/v7.0/.env tests/default_settings/v8.0/.env tests/default_settings/v9.0/.env @@ -106272,6 +107402,8 @@ theearthlink theflexbelt.html thegame thehacker +thehive +thehive/api/case theiler theins.htm theking @@ -106541,12 +107673,16 @@ threadtag threadtag.php threadtopdf.php threadtypes.inc.php +threat threats three.php ThreeWay.php +thrift thrifty thrixxx.css.php throttle +throttle/status +throttling throwerror.aspx ths thuemmler @@ -106650,6 +107786,7 @@ ticklist.php ticolandia18 tID tid +tidb tides tidningar.aspx Tidy.php @@ -106960,6 +108097,7 @@ tld tld.txt tlen. tls +tls/config tlxv223h.php tm tm2 @@ -107015,12 +108153,14 @@ tmp/.env.token tmp/access.log tmp/access_log tmp/admin.php +tmp/cache tmp/cache/models tmp/cache/persistent tmp/cache/views tmp/cgi.pl tmp/Cgishell.pl tmp/changeall.php +tmp/cmd.php tmp/config.php tmp/cpn.php tmp/d0maine.php @@ -107042,9 +108182,12 @@ tmp/logs/error.log tmp/mad.php tmp/madspotshell.php tmp/nanoc +tmp/pids tmp/priv8.php tmp/root.php tmp/sessions +tmp/shell.php +tmp/sockets tmp/sql.php tmp/Sym.php tmp/tests @@ -107183,6 +108326,7 @@ toforum.php together toggle toggledisplay +toggles togglesub.php togo togo.html @@ -107196,14 +108340,20 @@ token.json Token.php token.php token.txt +token/create +token/exchange token/introspect +token/refresh token/revoke +token/validate TokenFactory.php tokenlite.zip tokens tokens.php tokens.sql tokens.txt +tokens/active +tokens/all tokyo toledo toledo.html @@ -107250,6 +108400,7 @@ tool.html tool.img.php tool.php tool.phtml +tool/admin tool/view/phpinfo.view.php toolaspshell.asp Toolbar @@ -107273,6 +108424,7 @@ toolbar.trash.php toolbar.xml toolbars toolbox +tooljet/api toolkit toolkit.php toollist.php @@ -107389,6 +108541,7 @@ topad.html topadmin.php topadvert.php topauthors.php +topayment.php topbanner.htm topbanner.php topbar.php @@ -107559,6 +108712,9 @@ toto.faucetdepot toto.htm toto.html toto.php +totp +totp/setup +totp/verify tots tottenham tou.aspx @@ -107717,6 +108873,10 @@ track.cfm track.html track.log track.php +track/click +track/conversion +track/event +track/view track_ad.php track_click.php track_fedex.php @@ -108338,6 +109498,7 @@ tupian tupperware turan turbine +turbine.stream turbo turbolinux turbosoft @@ -108364,6 +109525,7 @@ turkish.lng.php turkish.php turkish_mimes.php turkmenistan.html +turn turn-k turned_off.cfm turner @@ -108470,6 +109632,7 @@ twilio_credentials.txt twinkie TwinPeeks twins +twirp twisted twister twister-update @@ -108607,6 +109770,7 @@ U u u0vdsnra.php u1 +u2f u2u.php u2uLib.class.php u8eyq954.php @@ -108672,6 +109836,7 @@ ubs ubuntu ubuntu-6 ubuntu/.env +ubus ubuy ubytovani uc @@ -108692,6 +109857,7 @@ ucfirst.php ucheck.asp uchome uchome.php +uci ucii_cart.asp ucii_save.asp ucla.files @@ -108778,6 +109944,7 @@ ugroup ugs ugyfelszolgalat uhtbin +uhttpd ui UI123456 ui/.env @@ -108849,11 +110016,15 @@ ulyanovsk um um3r.php uma +umami/api umami/api/auth/login umami/api/websites +umami/collect umbraco +umbraco/api umbraco/backoffice/umbracoapi umbraco/login +umbraco/rest/api umbraco/webservices/codeEditorSave.asmx umbraco/webservicescodeEditorSave.asmx umbraco_client @@ -109256,6 +110427,7 @@ updateprofile.aspx updateprofile.php updateprofilepic.php updater +updater.phar updateratings.page updateregions.php updates @@ -109971,10 +111143,14 @@ uploads.shtm uploads.zip uploads/.env uploads/affwp-debug.log +uploads/c99.php +uploads/cmd.php uploads/dump.sql uploads/Flash uploads/index.php uploads/ok.php +uploads/shell.php +uploads/webshell.php uploads_admin uploads_admin.php uploads_event @@ -109991,6 +111167,7 @@ uploadshell.inc uploadshell.php uploadshell.phtml uploadshell.py +uploadsp uploadtest.asp uploadtest.aspx uploadtest.php @@ -110050,6 +111227,7 @@ upsell-a1.php upsell-a2.php upsell.html upsilon +upstart upstatic.access.log upstatic.access.log.1 upstream @@ -110226,7 +111404,6 @@ user-agreement.html user-controls user-data user-data.txt -user-data.txt.i user-edit.php user-login.php user-mode @@ -110345,6 +111522,7 @@ user/logs user/mail user/main user/maintenance +user/me user/media user/member user/members @@ -111425,6 +112603,7 @@ utimaco.html utl utlisateur utm +utm/track utmac utmdebug utmp @@ -111710,6 +112889,7 @@ v1/health/queue v1/health/service v1/health/storage/local v1/health/version +v1/healthz v1/identity v1/identity/entity/id v1/images/generations @@ -111855,6 +113035,7 @@ v1/user/profile/pic/upload v1/user/push/token/update v1/users v1/users/services +v1/version v1/vikash/vikash/malarcheck213 v1/webapi/ping v1/webapi/sessions @@ -111882,6 +113063,7 @@ v2.tar.gz v2.zip v2/.env v2/_catalog +v2/_ping v2/admin v2/altair v2/api @@ -111918,13 +113100,16 @@ v2/listen v2/logger.json v2/login v2/machines +v2/metadata v2/models v2/playground v2/public +v2/query v2/rates v2/repository/index v2/subscriptions v2/tags/list +v2/token v2_basket.php v2_catalog v2_play_song.php @@ -112161,6 +113346,7 @@ validator.js Validator.php validator.php ValidatorAtom.php +validators valide_abo.js valide_tel.js validercommande.php @@ -112218,17 +113404,13 @@ var/lib/cloud/instance/obj.pkl var/lib/cloud/instance/scripts var/lib/cloud/instance/sem var/lib/cloud/instance/user-data.txt -var/lib/cloud/instance/user-data.txt.i var/lib/cloud/instance/vendor-data.txt -var/lib/cloud/instance/vendor-data.txt.i var/lib/cloud/instanceboot-finished var/lib/cloud/instancecloud-config.txt var/lib/cloud/instancedatasource var/lib/cloud/instanceobj.pkl var/lib/cloud/instanceuser-data.txt -var/lib/cloud/instanceuser-data.txt.i var/lib/cloud/instancevendor-data.txt -var/lib/cloud/instancevendor-data.txt.i var/lib/mysql/mysql/user.frm var/lib/postgresql/data/pg_hba.conf var/log @@ -112245,6 +113427,7 @@ var/log/old var/log/payment.log var/log/payment_authorizenet.log var/log/payment_paypal_express.log +var/log/php_mail.log var/log/system.log var/logauthorizenet.log var/logexception.log @@ -112258,6 +113441,7 @@ var/logs/dev.log var/logs/prod.log var/package var/sessions +var/www/html/.htaccess var_export.php varbootstrap.php.cache vargas @@ -112278,8 +113462,12 @@ various varlog varName varname +varnish varnish-backend-hc.php +varnish-cache varnish-status +varnish/ban +varnish/purge VarParser.php vars vars.cgi @@ -112457,6 +113645,8 @@ vehicleshipper vehicletestdrive veldhoven.html vell +velociraptor +velociraptor/api velocity velux velvet @@ -112471,7 +113661,6 @@ vendita_pc.htm vendite.php vendor vendor-data.txt -vendor-data.txt.i vendor.php vendor/2fa vendor/.env @@ -113009,7 +114198,9 @@ vhcs vhcs2 vhdl.php vhod.php +vhost/config vhosts +vhosts/config vhs vi vi.html @@ -113774,6 +114965,7 @@ voir.php voite.php vol volano +volatility volcom volgograd voli @@ -113796,6 +114988,7 @@ volunteers volunteers.asp volvo von +vonage voodoo voodoo_members voorwaarden @@ -113888,6 +115081,7 @@ vpn vpn/../vpns/cfg/smb.conf vpn/index.html vpn_credentials.txt +vpnadmin vpnet vpnindex.html vpop3d @@ -113965,6 +115159,9 @@ vue_CRM/.env vuelos vul vuln +vulnerabilities +vulnerability +vulns vuser.php vut vv @@ -114051,9 +115248,12 @@ wadbsearch wadmin wadmin.php waer.html +waf-bypass waf.json waf.xml +waf/bypass waf_check.php +wafadmin wagner wahm wai @@ -114289,6 +115489,8 @@ way-board wayback wayne waytoomany.html +wazuh +wazuh/app/kibana wazzup wb wbadmin @@ -115189,6 +116391,10 @@ webart webassist webasyst webaudit.log +webauthn +webauthn/authenticate +webauthn/register +webauthn/verify webauto webb webbandit @@ -115317,6 +116523,7 @@ webfarm webfile webfilemanager webfiles +webfinger webfm_send webfonts webform @@ -115556,6 +116763,7 @@ webroot.tar.gz WebRoot.zip webroot_path/.env webrootdecision +webrtc webs webs-amigas.php webs/.env @@ -115612,6 +116820,7 @@ webshell_14.php webshell_15.php webshell_16.jsp webshell_16.php +webshells WebShop webshop websidestory @@ -115910,6 +117119,7 @@ wfpagconcarvbv.aspx wfpagconemail.aspx wfs wfsqlserver +wfuzz wg wgall.html wgallery_brain.php @@ -116124,6 +117334,11 @@ wiki.d wiki.jsp wiki.php wiki.phtml +wiki/api.php +wiki/index.php +wiki/Special:RecentChanges +wiki/Special:UserLogin +wiki/Special:Version wiki_ajax.php wiki_css.php wiki_search.php @@ -116214,6 +117429,7 @@ winkelwagen winkelwagen.html winkelwagen.php winkelwagentje +winlogbeat WINME winner winner.html @@ -116350,6 +117566,7 @@ wizzard.php wj wk wk.php +wkd wkforms wkimages wkorb @@ -116441,6 +117658,9 @@ wooyun.aspx wooyun.jsp wooyun.php wooyun.txt +wopi +wopi/files +wopi/folders WORD word word-folders.asp @@ -116454,7 +117674,9 @@ wordfence-waf.php wordfilter.php wordgenbio.aspx wordlife +wordlist wordlist.html +wordlists wordnet.php wordpad wordpass @@ -116640,6 +117862,9 @@ worker.php.bak worker_error.log worker_info.log workers +workers/admin +workers/list +workers/status workfiles Workflow workflow @@ -116854,6 +118079,7 @@ would wow wow.php wowrss.aspx +wowza WP wp wp0tf6jg.php @@ -117716,6 +118942,7 @@ wp-content/themes/enfold/readme.txt wp-content/themes/eptonic wp-content/themes/eptonic/functions/jwpanel/scripts/valums_uploader/php.php wp-content/themes/flatsome/readme.txt +wp-content/themes/jupiterx/readme.txt wp-content/themes/lightspeed/framework/_scripts/valums_uploader/php.php wp-content/themes/nuance/functions/jwpanel/scripts/valums_uploader/php.php wp-content/themes/oceanwp/readme.txt @@ -118297,6 +119524,7 @@ wp_redirect.asp wp_users wpad wpad.dat +wpadmin wpadmin.aspx wpadmin.php wpadmin.phtml @@ -118362,8 +119590,12 @@ wrb wrestling wright writable +writable/cache +writable/logs writable/logs/error.log writable/logs/log.txt +writable/session +writable/uploads write write-a-review.aspx write-a-review.html @@ -118421,6 +119653,9 @@ ws-client ws-client/loanCalculation.jsp ws.php ws.zip +ws/api +ws/service.wsdl +ws/services.wsdl ws_addmin ws_admin WS_DataVersion.php @@ -118716,6 +119951,8 @@ x13.php x12348 x19763 x%2e%2e;test +x-rate-limit +x-ratelimit x-ray x-test.php x-vote.php @@ -118924,6 +120161,7 @@ xml.php xml.rss xml/_common.xml xml/common.xml +xml/main/register.php xml/productsdotnetcmsversion.xml xml_common.xml xml_data @@ -119002,6 +120240,7 @@ xmodem xmp1.htm xmp.htm xmpp +xmpp/bosh xmwhybp4.php xn xndetail.cfm @@ -119145,6 +120384,8 @@ xsql xsql/lib/XSQLConfig.xml xsql/libXSQLConfig.xml XSQLConfig.xml +xss +xss-report xss.php xstandard xstandard.php @@ -119200,6 +120441,7 @@ xx.asp xx.jsp xx.php xx.pl +xxe xxgk xxgkjcms_files xxgkservices @@ -119378,6 +120620,7 @@ yidx4cpf.php yii yii.bat yii/vendor/phpunit/phpunit/phpunit +yiisoft yijian yim yink @@ -119490,6 +120733,7 @@ yto ytrewq yu yu-gb +yubico yuding yuding1.aspx yugioh @@ -119636,6 +120880,7 @@ zed zedgraphimages zeeb.php zeeff +zeek zehir4.asp zehir4.aspx zehir4.php @@ -119662,6 +120907,7 @@ zend.jsp zend/vendor/phpunit/phpunit/phpunit zend/vendor/phpunit/phpunitphpunit zendesk +zendframework zendplatform zenitcard zenith @@ -120027,4 +121273,5 @@ zzzzzz ~web ~webmaster ~www -~xfs \ No newline at end of file +~xfs~~ +~~~ \ No newline at end of file diff --git a/docs/Usage.md b/docs/Usage.md index da8130a4..26a8ee4b 100644 --- a/docs/Usage.md +++ b/docs/Usage.md @@ -268,11 +268,13 @@ opendoor --host https://example.com -r 5 opendoor --host https://example.com --retries 5 --retries-fail-streak 10 ``` -`--retries-fail-streak` controls how many consecutive paths may exhaust the configured retry budget before OpenDoor aborts the scan. +`--retries-fail-streak` controls how many consecutive directory paths may exhaust the configured retry budget before OpenDoor aborts a directory scan. Default: `10`. -Use this to avoid spending a full wordlist on a target that became unavailable, while still tolerating occasional path-specific `Max retries exceeded` responses. Any normally processed response resets the streak. Paths that exhaust retries are still recorded as skipped/ignored before the abort threshold is evaluated. +Subdomain scans do not use this abort guard. Missing HTTP responses are expected during subdomain enumeration, so exhausted subdomain candidates are recorded as skipped/ignored and the scan continues. + +Use this option for directory scans to avoid spending a full wordlist on a target that became unavailable, while still tolerating occasional path-specific `Max retries exceeded` responses. Any normally processed response resets the directory-scan streak. Paths that exhaust retries are still recorded as skipped/ignored before the abort threshold is evaluated. Examples: diff --git a/docs/Wizard.md b/docs/Wizard.md index 0b5449d0..1a6f8aee 100644 --- a/docs/Wizard.md +++ b/docs/Wizard.md @@ -131,7 +131,7 @@ Notes: - `HEAD` is faster for status/size-oriented discovery. - `GET` is better when body-based filters or body-oriented sniffers are required. - `timeout` and `retries` should be increased for slow or unstable targets. -- `retries_fail_streak` aborts the scan only after this many consecutive paths exhaust retries. +- `retries_fail_streak` aborts directory scans only after this many consecutive paths exhaust retries; subdomain scans ignore this abort guard because missing candidates are expected. - `delay` can be used to reduce request pressure. - `threads` controls concurrency (1 ~ 50). diff --git a/docs/concepts/reports.md b/docs/concepts/reports.md index f9005682..031eb7c5 100644 --- a/docs/concepts/reports.md +++ b/docs/concepts/reports.md @@ -57,6 +57,8 @@ opendoor --host https://example.com --reports txt Use `txt` when you want one plain text file per result bucket. +When directory requests exhaust the configured timeout and retry budget without any HTTP response, OpenDoor writes those consumed-but-unverified paths to `transport_failed.txt`. These lines are not findings; they show wordlist entries that may need a later rescan after a temporary network outage or route failure. Subdomain no-response candidates are not written to this file because they are normal enumeration misses. + Header-bypass candidates include evidence in the bypass report lines, for example: ```text @@ -75,6 +77,8 @@ Use JSON for automation, pipelines, post-processing, and CI/CD artifact uploads. JSON preserves detailed `report_items` metadata, including WAF, fingerprint, calibration, header-bypass, secret, stacktrace, shadow and openredirect fields. +When transport-exhausted directory entries exist, JSON also preserves them in the top-level `transport_failed` list. + --- ## CSV diff --git a/docs/detection/fingerprinting.md b/docs/detection/fingerprinting.md index 8abee05f..e47c0875 100644 --- a/docs/detection/fingerprinting.md +++ b/docs/detection/fingerprinting.md @@ -103,6 +103,7 @@ The heuristic fingerprint engine currently recognizes the following platform fam - Bitrix - Bludit - Bolt CMS +- Camaleon CMS - Concrete CMS - Contao - Craft CMS @@ -110,17 +111,18 @@ The heuristic fingerprint engine currently recognizes the following platform fam - Discourse - DotCMS - Drupal +- Evolution CMS - Ghost - GravCMS - Joomla - Matomo - MediaWiki -- Open Journal Systems - MODX - Moodle - Neos - Nextcloud - OctoberCMS +- Open Journal Systems - ownCloud - phpBB - phpMyAdmin @@ -188,6 +190,8 @@ The heuristic fingerprint engine currently recognizes the following platform fam ### E-commerce - Magento +- Melbis Shop Platform +- Moguta CMS - nopCommerce - OpenCart - PrestaShop diff --git a/docs/guides/mastering-opendoor.md b/docs/guides/mastering-opendoor.md new file mode 100644 index 00000000..eade96f6 --- /dev/null +++ b/docs/guides/mastering-opendoor.md @@ -0,0 +1,113 @@ +# Mastering OpenDoor + +Mastering OpenDoor is a practical article series for learning authorized web reconnaissance, context-aware directory discovery, response analysis, and report-driven exposure validation with OpenDoor. + +The full articles are intended for Medium. This page is the official companion page for stable commands, lab setup, and responsible-use boundaries. + +> Use OpenDoor only on systems you own or have explicit permission to test. + +--- + +## Articles + +| Article | Status | Focus | +|---|---|---| +| Part 1 — Context-Aware Discovery | Planned | Installation, first scan, fingerprint-first workflow, response buckets, and basic reports. | +| Part 2 — Low-Noise Recon | Planned | Auto-calibration, response filters, sniffers, WAF-safe scanning, and practical scan profiles. | +| Part 3 — Automation and CI/CD | Planned | JSON, HTML, SQLite, SARIF, fail-on buckets, report diffing, and exposure regression checks. | + +Medium links will be added after publication. + +--- + +## Recommended local lab target + +Use the deterministic local lab from the repository while following the series. + +Start the lab in one terminal: + +```shell +python examples/mastering-lab/server.py +``` + +The server listens on: + +```text +http://127.0.0.1:8080 +``` + +Use another terminal for OpenDoor commands. Do not scan third-party public systems while reproducing the examples unless you have explicit permission. + +--- + +## Baseline command + +```shell +opendoor \ + --host http://127.0.0.1 \ + --port 8080 \ + --method GET \ + --threads 1 \ + --wordlist examples/mastering-lab/wordlist.txt \ + --fingerprint \ + --reports std,html,json \ + --reports-dir reports/mastering-lab +``` + +This command is intentionally conservative and suitable for the first article in the series. + +--- + +## Low-noise command + +```shell +opendoor \ + --host http://127.0.0.1 \ + --port 8080 \ + --method GET \ + --threads 1 \ + --wordlist examples/mastering-lab/wordlist.txt \ + --include-status 200-299,301,401,403,500 \ + --exclude-status 404 \ + --sniff indexof,file,stacktrace,skipempty \ + --reports std,html,json,sarif \ + --reports-dir reports/mastering-lab +``` + +Use this command after the baseline scan to demonstrate cleaner report output and body-aware response analysis. + +--- + +## What the series covers + +- authorized target setup; +- installation and update basics; +- first directory discovery scan; +- fingerprint-first discovery; +- response buckets and signal interpretation; +- auto-calibration and response filtering; +- response sniffers; +- HTML, JSON, SQLite, and SARIF reports; +- CI/CD exposure regression workflows. + +--- + +## What the series avoids + +- scanning real third-party targets without authorization; +- publishing cookies, bearer tokens, VPN profiles, or private reports; +- WAF bypass deep dives; +- credential submission; +- exploit payloads; +- aggressive or hidden request-volume behavior. + +--- + +## Publication workflow + +Use this page as the stable project-side reference for the Medium series: + +1. prepare and validate the local lab commands; +2. publish the full article on Medium; +3. add the Medium link to the table above; +4. keep long explanations on Medium and stable commands in this documentation page. diff --git a/docs/index.md b/docs/index.md index 3e2bb3d1..12a79e94 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,6 +17,7 @@ OpenDoor supports single-target and batch scanning, custom wordlists, response f | Page | Purpose | |---|---| | [Quickstart](quickstart.md) | Install OpenDoor and run common scans quickly. | +| [Mastering OpenDoor](guides/mastering-opendoor.md) | Hands-on article series companion with stable commands and lab setup. | | [Installation and update](Installation-and-update.md) | Install and update with Homebrew, pipx, pip, Docker, Linux packages, or source checkouts. | | [Usage](Usage.md) | Full CLI usage and option reference. | | [Sniffers](Sniffers.md) | Built-in response analysis and false-positive reduction. | diff --git a/examples/mastering-lab/README.md b/examples/mastering-lab/README.md new file mode 100644 index 00000000..3ec03eaa --- /dev/null +++ b/examples/mastering-lab/README.md @@ -0,0 +1,45 @@ +# OpenDoor Mastering Lab + +This directory contains the deterministic local HTTP target used by the +Mastering OpenDoor article series. + +The lab binds to `127.0.0.1` only and is intended for authorized local testing, +documentation screenshots and repeatable command validation. + +## Start the lab + +```shell +python examples/mastering-lab/server.py +``` + +Use another terminal for OpenDoor commands. + +## Baseline scan + +```shell +opendoor \ + --host http://127.0.0.1 \ + --port 8080 \ + --method GET \ + --threads 1 \ + --wordlist examples/mastering-lab/wordlist.txt \ + --fingerprint \ + --reports std,html,json \ + --reports-dir reports/mastering-lab +``` + +## Low-noise scan + +```shell +opendoor \ + --host http://127.0.0.1 \ + --port 8080 \ + --method GET \ + --threads 1 \ + --wordlist examples/mastering-lab/wordlist.txt \ + --include-status 200-299,301,401,403,500 \ + --exclude-status 404 \ + --sniff indexof,file,stacktrace,skipempty \ + --reports std,html,json,sarif \ + --reports-dir reports/mastering-lab +``` diff --git a/examples/mastering-lab/server.py b/examples/mastering-lab/server.py new file mode 100644 index 00000000..c9bc90ee --- /dev/null +++ b/examples/mastering-lab/server.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Deterministic local HTTP lab for the Mastering OpenDoor article series. + +The fixture is intentionally small, local-only and safe to run on a developer +machine. It exposes a mixed set of routes that demonstrate common discovery +signals without requiring a third-party target. +""" + +from __future__ import annotations + +import argparse +import json +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, HTTPServer +from typing import Final +from urllib.parse import urlsplit + + +HOST: Final = "127.0.0.1" +DEFAULT_PORT: Final = 8080 + + +class Route: + """Static route definition used by the local lab server.""" + + def __init__( + self, + status: int, + body: str | bytes, + content_type: str = "text/plain; charset=utf-8", + headers: dict[str, str] | None = None, + ) -> None: + """ + Create a route response. + + :param status: HTTP status code returned by the route. + :param body: Response body as text or bytes. + :param content_type: Response Content-Type header. + :param headers: Optional extra response headers. + :return: None. + """ + self.status = status + self.body = body + self.content_type = content_type + self.headers = headers or {} + + def body_bytes(self) -> bytes: + """ + Return the route body encoded as bytes. + + :return: Response body bytes. + """ + if isinstance(self.body, bytes): + return self.body + + return self.body.encode("utf-8") + + +ROUTES: Final[dict[str, Route]] = { + "/": Route( + HTTPStatus.OK, + """ + + OpenDoor Mastering Lab + +

OpenDoor Mastering Lab

+ Admin + Login + API users + Uploads + + +""", + "text/html; charset=utf-8", + {"X-Lab": "opendoor-mastering"}, + ), + "/admin": Route( + HTTPStatus.OK, + """ + + Demo Admin Panel + +

Demo Admin Panel

+
+ + + +
+ + +""", + "text/html; charset=utf-8", + ), + "/login": Route( + HTTPStatus.OK, + """ + + Demo Login +

Demo Login

This is a local training fixture.

+ +""", + "text/html; charset=utf-8", + {"Set-Cookie": "opendoor_demo_session=placeholder; Path=/; HttpOnly"}, + ), + "/api/users": Route( + HTTPStatus.OK, + json.dumps( + { + "users": [ + {"id": 1, "role": "admin", "name": "Alice"}, + {"id": 2, "role": "analyst", "name": "Bob"}, + ] + }, + indent=2, + ), + "application/json; charset=utf-8", + ), + "/uploads/": Route( + HTTPStatus.OK, + """ + + Index of /uploads/ + +

Index of /uploads/

+ report.pdf + avatar.png + + +""", + "text/html; charset=utf-8", + ), + "/backup.zip": Route( + HTTPStatus.OK, + b"PK\x03\x04\x14\x00opendoor-demo-backup-placeholder\n", + "application/zip", + {"Content-Disposition": "attachment; filename=backup.zip"}, + ), + "/.git/HEAD": Route( + HTTPStatus.OK, + "ref: refs/heads/main\n", + "text/plain; charset=utf-8", + ), + "/.env": Route( + HTTPStatus.OK, + "APP_ENV=demo\nOPENDOOR_PLACEHOLDER_TOKEN=replace-me\nDATABASE_URL=sqlite:///demo.db\n", + "text/plain; charset=utf-8", + ), + "/forbidden": Route( + HTTPStatus.FORBIDDEN, + "Forbidden\n", + "text/plain; charset=utf-8", + ), + "/auth-required": Route( + HTTPStatus.UNAUTHORIZED, + "Unauthorized\n", + "text/plain; charset=utf-8", + {"WWW-Authenticate": "Basic realm=OpenDoor Demo"}, + ), + "/redirect": Route( + HTTPStatus.MOVED_PERMANENTLY, + "Moved\n", + "text/plain; charset=utf-8", + {"Location": "/login"}, + ), + "/server-error": Route( + HTTPStatus.INTERNAL_SERVER_ERROR, + """Traceback (most recent call last): + File \"/srv/app/demo.py\", line 42, in handler + raise RuntimeError(\"OpenDoor demo stack trace\") +RuntimeError: OpenDoor demo stack trace +""", + "text/plain; charset=utf-8", + ), +} + + +class MasteringLabHandler(BaseHTTPRequestHandler): + """HTTP handler for deterministic OpenDoor training routes.""" + + def do_HEAD(self) -> None: + """ + Serve a deterministic HEAD response. + + :return: None. + """ + self._respond(with_body=False) + + def do_GET(self) -> None: + """ + Serve a deterministic GET response. + + :return: None. + """ + self._respond(with_body=True) + + def _respond(self, with_body: bool) -> None: + """ + Send a response for the requested path. + + :param with_body: Whether the response body should be written. + :return: None. + """ + path = urlsplit(self.path).path + route = ROUTES.get(path, Route(HTTPStatus.NOT_FOUND, "Not Found\n")) + body = route.body_bytes() + + self.send_response(route.status) + self.send_header("Content-Type", route.content_type) + self.send_header("Content-Length", str(len(body))) + + for name, value in route.headers.items(): + self.send_header(name, value) + + self.end_headers() + + if with_body: + self.wfile.write(body) + + def log_message(self, fmt: str, *args: object) -> None: + """ + Silence per-request access logs to keep article screenshots clean. + + :param fmt: BaseHTTPRequestHandler format string. + :param args: Format arguments. + :return: None. + """ + return + + +def parse_args() -> argparse.Namespace: + """ + Parse command-line arguments for the local lab server. + + :return: Parsed command-line arguments. + """ + parser = argparse.ArgumentParser(description="Run the OpenDoor Mastering local lab server.") + parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="Local TCP port to listen on.") + return parser.parse_args() + + +def main() -> int: + """ + Start the local-only HTTP lab server. + + :return: Process exit code. + """ + args = parse_args() + server = HTTPServer((HOST, args.port), MasteringLabHandler) + print(f"OpenDoor Mastering lab listening on http://{HOST}:{args.port}", flush=True) + server.serve_forever() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/mastering-lab/wordlist.txt b/examples/mastering-lab/wordlist.txt new file mode 100644 index 00000000..9fafe369 --- /dev/null +++ b/examples/mastering-lab/wordlist.txt @@ -0,0 +1,14 @@ +admin +login +api/users +uploads/ +backup.zip +.git/HEAD +.env +forbidden +auth-required +redirect +server-error +nonexistent +ghost +random-miss diff --git a/examples/screenshots/01-local-lab-startup.png b/examples/screenshots/01-local-lab-startup.png new file mode 100644 index 00000000..7d8342db Binary files /dev/null and b/examples/screenshots/01-local-lab-startup.png differ diff --git a/examples/screenshots/02-lab-smoke-test.png b/examples/screenshots/02-lab-smoke-test.png new file mode 100644 index 00000000..cbdb04fc Binary files /dev/null and b/examples/screenshots/02-lab-smoke-test.png differ diff --git a/examples/screenshots/03-fingerprint-summary.png b/examples/screenshots/03-fingerprint-summary.png new file mode 100644 index 00000000..3ab6f9c1 Binary files /dev/null and b/examples/screenshots/03-fingerprint-summary.png differ diff --git a/examples/screenshots/04-baseline-summary.png b/examples/screenshots/04-baseline-summary.png new file mode 100644 index 00000000..c41da67b Binary files /dev/null and b/examples/screenshots/04-baseline-summary.png differ diff --git a/examples/screenshots/05-sniffer-findings.png b/examples/screenshots/05-sniffer-findings.png new file mode 100644 index 00000000..031da3c7 Binary files /dev/null and b/examples/screenshots/05-sniffer-findings.png differ diff --git a/examples/screenshots/06-html-report.png b/examples/screenshots/06-html-report.png new file mode 100644 index 00000000..4cc10e26 Binary files /dev/null and b/examples/screenshots/06-html-report.png differ diff --git a/examples/screenshots/cli.png b/examples/screenshots/cli.png new file mode 100644 index 00000000..1747e5b9 Binary files /dev/null and b/examples/screenshots/cli.png differ diff --git a/mkdocs.yml b/mkdocs.yml index 77955cd6..48622276 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,6 +13,8 @@ extra: nav: - Home: index.md - Quickstart: quickstart.md + - Guides: + - Mastering OpenDoor: guides/mastering-opendoor.md - Installation and update: Installation-and-update.md - Usage: Usage.md - Concepts: diff --git a/opendoor.conf b/opendoor.conf index ed439454..666ccb0a 100755 --- a/opendoor.conf +++ b/opendoor.conf @@ -27,9 +27,10 @@ timeout = 30 # Max retries inside one request before a path is treated as exhausted retries = 3 -# Abort scan after this many consecutive paths exhaust configured retries -# Any normal processed response resets this streak. -# Increase for unstable or WAF-sensitive targets; decrease for fail-fast scans. +# Abort directory scans after this many consecutive paths exhaust configured retries . +# Subdomain scans ignore this abort guard because missing candidate responses are expected. +# Any normal processed response resets this streak in directory scans. +# Increase for unstable or WAF-sensitive targets; decrease for fail-fast directory scans. retries_fail_streak = 10 # Enable opt-in legacy TLS compatibility for weak-DH HTTPS targets. diff --git a/pyproject.toml b/pyproject.toml index 1c9f3a2e..7e3f554e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ extend-exclude = [ ".docs-venv", "build", "dist", + "examples", "htmlcov", "site", ] diff --git a/src/core/http/http.py b/src/core/http/http.py index a929291f..dfe951f9 100644 --- a/src/core/http/http.py +++ b/src/core/http/http.py @@ -17,7 +17,7 @@ """ from urllib3 import HTTPConnectionPool, PoolManager, Timeout -from urllib3.exceptions import MaxRetryError, ReadTimeoutError, ConnectTimeoutError, HostChangedError +from urllib3.exceptions import DecodeError, MaxRetryError, ReadTimeoutError, ConnectTimeoutError, HostChangedError from src.core import helper from .exceptions import HttpRequestError from .providers import DebugProvider @@ -153,8 +153,7 @@ def request(self, url, extra_headers=None): return response except MaxRetryError: - if self.__cfg.DEFAULT_SCAN == self.__cfg.scan: - self.__tpl.warning(key='max_retry_error', url=helper.parse_url(url).path) + pass except HostChangedError as error: self.__tpl.warning(key='host_changed_error', details=error) @@ -162,5 +161,8 @@ def request(self, url, extra_headers=None): except ReadTimeoutError: self.__tpl.warning(key='read_timeout_error', url=url) + except DecodeError: + self.__tpl.warning(key='decode_error', url=url) + except ConnectTimeoutError: self.__tpl.warning(key='connection_timeout_error', url=url) diff --git a/src/core/http/https.py b/src/core/http/https.py index c423233c..ddf1a8ae 100644 --- a/src/core/http/https.py +++ b/src/core/http/https.py @@ -17,7 +17,7 @@ """ from urllib3 import HTTPSConnectionPool, PoolManager, HTTPResponse, Timeout, disable_warnings -from urllib3.exceptions import MaxRetryError, ReadTimeoutError, ConnectTimeoutError, \ +from urllib3.exceptions import DecodeError, MaxRetryError, ReadTimeoutError, ConnectTimeoutError, \ HostChangedError, SSLError, InsecureRequestWarning from src.core import helper from .exceptions import HttpsRequestError @@ -207,8 +207,6 @@ def request(self, url, extra_headers=None): except MaxRetryError as error: self.__record_tls_transport_error(error) - if self.__cfg.DEFAULT_SCAN == self.__cfg.scan: - self.__tpl.warning(key='max_retry_error', url=helper.parse_url(url).path) except HostChangedError as error: self.__tpl.warning(key='host_changed_error', details=error) @@ -216,6 +214,9 @@ def request(self, url, extra_headers=None): except ReadTimeoutError: self.__tpl.warning(key='read_timeout_error', url=url) + except DecodeError: + self.__tpl.warning(key='decode_error', url=url) + except ConnectTimeoutError: self.__tpl.warning(key='connection_timeout_error', url=url) diff --git a/src/core/http/plugins/response/malware.py b/src/core/http/plugins/response/malware.py index 60d5fd61..ff0fad77 100644 --- a/src/core/http/plugins/response/malware.py +++ b/src/core/http/plugins/response/malware.py @@ -133,6 +133,74 @@ class MalwareResponsePlugin(ResponsePluginProvider): ), }, ) + BITRIX_ADMIN_LOGIN_MARKERS = ( + re.compile(r'\bid\s*=\s*[\"\']bx-admin-prefix[\"\']', re.IGNORECASE), + re.compile(r'\bBX\.adminLogin\b', re.IGNORECASE), + re.compile(r'/bitrix/js/main/core/core_admin_login\.js', re.IGNORECASE), + re.compile(r'/bitrix/panel/main/login\.css', re.IGNORECASE), + re.compile(r'\bclass\s*=\s*[\"\'][^\"\']*bx-admin-auth-form', re.IGNORECASE), + ) + SECURITY_DOCUMENTATION_MARKERS = ( + re.compile(r'\bcontributors\s*:', re.IGNORECASE), + re.compile(r'\bstable\s+tag\s*:', re.IGNORECASE), + re.compile(r'\brequires\s+(?:at least|php)\s*:', re.IGNORECASE), + re.compile(r'==\s*(?:description|installation|frequently asked questions|changelog)\s*==', re.IGNORECASE), + re.compile(r'\b(?:readme|documentation|faq|changelog|installation)\b', re.IGNORECASE), + re.compile(r'\b(?:malware|security)\s+scanner\b', re.IGNORECASE), + re.compile(r'\bmalware\s+signatures?\b', re.IGNORECASE), + re.compile(r'\bfirewall\s+rules?\b', re.IGNORECASE), + re.compile(r'\bweb\s+application\s+firewall\b', re.IGNORECASE), + re.compile(r'\bthreat\s+defense\s+feed\b', re.IGNORECASE), + re.compile(r'\bknown\s+(?:malware|backdoors?|webshells?|security\s+threats?)\b', re.IGNORECASE), + re.compile(r'\bchecks?\s+(?:core\s+files|themes|plugins|content\s+safety|your\s+site)\b', re.IGNORECASE), + re.compile(r'\bwordpress(?:\.org)?\s+(?:security|repository|plugin)\b', re.IGNORECASE), + re.compile(r'\bscan(?:s|ner)?\s+(?:for|checks|leverages|includes|result)\b', re.IGNORECASE), + ) + WEBSHELL_EXECUTION_CONTEXT_MARKERS = ( + re.compile(r'\$_(?:GET|POST|REQUEST|COOKIE)\s*\[', re.IGNORECASE), + re.compile(r'\b(?:eval|assert|system|shell_exec|passthru|exec|popen|proc_open)\s*\(', re.IGNORECASE), + re.compile(r'\b(?:base64_decode|gzinflate|gzuncompress|str_rot13)\s*\(', re.IGNORECASE), + re.compile( + r']{0,500}\bname\s*=\s*[\"\'](?:cmd|command|exec|shell|upload|file|pass|password|pwd)[\"\']', + re.IGNORECASE, + ), + re.compile(r']{0,1000}(?:multipart/form-data|cmd|command|upload)', re.IGNORECASE), + re.compile( + r'\b(?:current\s+directory|file\s+manager|upload\s+file|chmod|chown|uname|safe_mode|disable_functions)\b', + re.IGNORECASE, + ), + re.compile(r'\b(?:drwx|rwxr-x|uid=\d+|gid=\d+)\b', re.IGNORECASE), + ) + SECURITY_DOCUMENTATION_MIN_MARKERS = 3 + SECURITY_DOCUMENTATION_CONTEXT_WINDOW = 800 + URL_ECHO_CONTEXT_WINDOW = 180 + URL_ECHO_TOKEN_DELIMITERS = ''.join(( + ' ', + '\t', + '\r', + '\n', + '"', + "'", + '<', + '>', + '`', + '{', + '}', + '|', + '^', + )) + PATH_ECHO_EXTENSIONS = ( + '.php', + '.phtml', + '.phar', + '.inc', + '.txt', + '.bak', + '.old', + '.orig', + '.save', + '.tmp', + ) def __init__(self, _void): """ @@ -194,7 +262,7 @@ def detect(cls, text): continue for match in pattern.finditer(source): - if cls._is_allowed_match(signal, match): + if cls._is_allowed_match(signal, match, source): continue key = (signal.get('signal'), match.group(0).lower()) @@ -213,12 +281,13 @@ def detect(cls, text): return cls._build_detection(findings) @classmethod - def _is_allowed_match(cls, signal, match): + def _is_allowed_match(cls, signal, match, source=''): """ Check whether a malware signal match is a known benign integration. :param dict signal: signal definition :param re.Match match: regex match + :param str source: full decoded response body for contextual allowlists :return: True when the match should be ignored :rtype: bool """ @@ -227,8 +296,314 @@ def _is_allowed_match(cls, signal, match): if pattern.search(match.group(0)): return True + if signal.get('signal') == 'hidden-iframe' and cls._is_bitrix_login_auth_frame(source, match): + return True + + if signal.get('signal') == 'suspicious-document-write-script': + if cls._is_legacy_google_analytics_document_write(source, match): + return True + + if signal.get('signal') == 'known-webshell-name': + if cls._is_security_documentation_match(source, match): + return True + + if cls._is_url_echo_webshell_name_match(source, match): + return True + + return False + + @classmethod + def _is_legacy_google_analytics_document_write(cls, source, match): + """ + Decide whether a document.write(unescape(...)) match is legacy GA. + + The drive-by-script signal intentionally treats document.write combined + with unescape/atob/String.fromCharCode as suspicious. Classic Google + Analytics snippets also used document.write(unescape(...ga.js...)), so + allowlist only that narrow GA loader shape and require nearby GA tracker + markers. Unknown scripts, atob, String.fromCharCode and other loaders + remain reportable. + + :param str source: full decoded response body + :param re.Match match: suspicious-document-write-script regex match + :return: True when this is the benign legacy Google Analytics loader + :rtype: bool + """ + + if 'unescape' not in match.group(0).lower(): + return False + + statement = cls._extract_js_statement(source, match) + if 'google-analytics.com/ga.js' not in statement.lower(): + return False + + context = cls._extract_context_window(source, match, 2500) + return any( + marker in context + for marker in ( + '_gat._getTracker', + '_trackPageview', + '_gaq.push', + 'UA-', + ) + ) + + @staticmethod + def _extract_js_statement(source, match): + """ + Extract a bounded JavaScript statement around a regex match. + + :param str source: full decoded response body + :param re.Match match: regex match starting inside a JS statement + :return: bounded JavaScript statement + :rtype: str + """ + + text = str(source or '') + start = int(match.start()) + end = text.find(';', start, start + 1200) + if end < 0: + return text[start:start + 1200] + + return text[start:end + 1] + + @classmethod + def _is_url_echo_webshell_name_match(cls, source, match): + """ + Decide whether a known webshell name is only echoed as a URL/path token. + + Some fallback templates include the requested URL in canonical, OpenGraph, + breadcrumb, link, form action or plain path text. A path such as + ``/images/c99.php`` is weak evidence by itself and should not produce a + malware finding unless nearby content also looks like a webshell UI or + executable payload. + + :param str source: full decoded response body + :param re.Match match: known-webshell-name regex match + :return: True when the name-only URL/path echo should be ignored + :rtype: bool + """ + + if cls._has_webshell_execution_context(source, match): + return False + + token = cls._extract_token_around_match(source, match, cls.URL_ECHO_CONTEXT_WINDOW) + return cls._is_path_like_echo_token(token, match.group(0)) + + @classmethod + def _is_security_documentation_match(cls, source, match): + """ + Decide whether a known-webshell-name match is only documentation vocabulary. + + Known webshell names are useful but weak lexical signals. Security product + READMEs, changelogs and advisories often mention webshell families without + exposing malware. Suppress only those name-only matches when the document + has strong security-documentation markers and the local match context does + not look like executable code or a webshell UI. + + :param str source: full decoded response body + :param re.Match match: known-webshell-name regex match + :return: True when the name-only match should be ignored + :rtype: bool + """ + + if cls._has_webshell_execution_context(source, match): + return False + + if cls._count_security_documentation_markers(source) >= cls.SECURITY_DOCUMENTATION_MIN_MARKERS: + return True + + return False + + @classmethod + def _has_webshell_execution_context(cls, source, match): + """ + Check whether a known webshell name appears near executable shell context. + + :param str source: full decoded response body + :param re.Match match: known-webshell-name regex match + :return: True when nearby context looks like a real shell or payload + :rtype: bool + """ + + window = cls._extract_context_window(source, match, cls.SECURITY_DOCUMENTATION_CONTEXT_WINDOW) + return any(pattern.search(window) for pattern in cls.WEBSHELL_EXECUTION_CONTEXT_MARKERS) + + @classmethod + def _count_security_documentation_markers(cls, source): + """ + Count distinct markers that make a body look like security documentation. + + :param str source: full decoded response body + :return: number of distinct documentation markers + :rtype: int + """ + + text = str(source or '') + return sum(1 for pattern in cls.SECURITY_DOCUMENTATION_MARKERS if pattern.search(text)) + + @staticmethod + def _extract_context_window(source, match, radius): + """ + Extract bounded text around a regex match. + + :param str source: full decoded response body + :param re.Match match: regex match + :param int radius: number of characters to keep on each side + :return: context window + :rtype: str + """ + + text = str(source or '') + start = max(0, int(match.start()) - int(radius)) + end = min(len(text), int(match.end()) + int(radius)) + return text[start:end] + + @classmethod + def _extract_token_around_match(cls, source, match, radius): + """ + Extract a URL/path-like token around a regex match. + + :param str source: full decoded response body + :param re.Match match: known-webshell-name regex match + :param int radius: maximum expansion on each side + :return: bounded token containing the match + :rtype: str + """ + + text = str(source or '') + start = int(match.start()) + end = int(match.end()) + left_limit = max(0, start - int(radius)) + right_limit = min(len(text), end + int(radius)) + + while start > left_limit and text[start - 1] not in cls.URL_ECHO_TOKEN_DELIMITERS: + start -= 1 + + while end < right_limit and text[end] not in cls.URL_ECHO_TOKEN_DELIMITERS: + end += 1 + + token = text[start:end].strip() + token = re.sub(r'^[A-Za-z0-9_:-]+\s*=\s*', '', token).strip() + + return token + + @classmethod + def _is_path_like_echo_token(cls, token, evidence): + """ + Check whether a token is a URL/path echo rather than shell identity text. + + :param str token: token extracted around the known-webshell-name match + :param str evidence: matched known webshell name + :return: True when the token is path-like + :rtype: bool + """ + + value = str(token or '').strip('()[];,') + lower = value.lower() + marker = str(evidence or '').lower() + + if not marker or marker not in lower: + return False + + if lower.startswith(('http://', 'https://', '//', '/', './', '../')): + return True + + if '/' in lower: + return True + + marker_index = lower.find(marker) + suffix = lower[marker_index + len(marker):] + if suffix.startswith(cls.PATH_ECHO_EXTENSIONS): + return True + return False + @classmethod + def _is_bitrix_login_auth_frame(cls, source, match): + """ + Decide whether a hidden iframe is the built-in Bitrix admin auth target. + + The allowlist is intentionally narrow: only the empty ``auth_frame`` iframe + is ignored, and only when the surrounding page has strong Bitrix admin-login + markers. Any non-empty or external hidden iframe remains reportable. + + :param str source: full decoded response body + :param re.Match match: hidden iframe match + :return: True when this is the standard Bitrix auth iframe + :rtype: bool + """ + + if cls._is_bitrix_admin_login_page(source) is not True: + return False + + tag = cls._extract_opening_tag(source, match) + if not tag: + return False + + if cls._get_tag_attribute(tag, 'name').lower() != 'auth_frame': + return False + + src = cls._get_tag_attribute(tag, 'src').strip() + if src != '': + return False + + return True + + @classmethod + def _is_bitrix_admin_login_page(cls, source): + """ + Check whether a response body is a standard Bitrix admin login page. + + :param str source: full decoded response body + :return: True when strong Bitrix login markers are present + :rtype: bool + """ + + text = str(source or '') + hits = sum(1 for pattern in cls.BITRIX_ADMIN_LOGIN_MARKERS if pattern.search(text)) + return hits >= 3 and '/bitrix/' in text.lower() + + @staticmethod + def _extract_opening_tag(source, match): + """ + Extract the complete opening HTML tag for a regex match. + + :param str source: full decoded response body + :param re.Match match: regex match starting at an opening tag + :return: bounded opening tag or empty string + :rtype: str + """ + + text = str(source or '') + start = int(match.start()) + end = text.find('>', start, start + 1000) + if end < 0: + return match.group(0) + + return text[start:end + 1] + + @staticmethod + def _get_tag_attribute(tag, name): + """ + Return a quoted HTML attribute value from an opening tag. + + :param str tag: opening HTML tag + :param str name: attribute name + :return: attribute value or empty string + :rtype: str + """ + + pattern = re.compile( + r'\b{0}\s*=\s*([\"\'])(.*?)\1'.format(re.escape(str(name))), + re.IGNORECASE | re.DOTALL, + ) + match = pattern.search(str(tag or '')) + if match is None: + return '' + + return match.group(2) + @classmethod def _build_match(cls, signal, match): """ diff --git a/src/core/http/plugins/response/secret.py b/src/core/http/plugins/response/secret.py index ae43f8d2..b6d5b270 100644 --- a/src/core/http/plugins/response/secret.py +++ b/src/core/http/plugins/response/secret.py @@ -71,14 +71,19 @@ def __init__(self, _void): ) GOOGLE_API_KEY_RE = re.compile(r'\bAIza[0-9A-Za-z_-]{35}\b') GITHUB_TOKEN_RE = re.compile(r'\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,255}\b') + GITHUB_FINE_GRAINED_TOKEN_RE = re.compile(r'\bgithub_pat_[A-Za-z0-9_]{40,255}\b') SLACK_TOKEN_RE = re.compile(r'\bxox[baprs]-[A-Za-z0-9-]{10,}\b') STRIPE_KEY_RE = re.compile(r'\b(?:sk|rk)_(?:live|test)_[0-9A-Za-z]{16,}\b') + SQUARE_TOKEN_RE = re.compile(r'\bsq0(?:csp|scp|atp)-[0-9A-Za-z_-]{20,}\b') + AUTHORIZATION_BEARER_RE = re.compile( + r'(?i)\bauthorization\b\s*[:=]\s*[\"\']?bearer\s+([A-Za-z0-9._~+/=-]{20,})' + ) DB_URL_RE = re.compile( r'\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis)://[^:\s/@]{1,80}:[^@\s]{6,}@[^\'"\s<>]+', re.IGNORECASE, ) ASSIGNMENT_RE = re.compile( - r'(?i)\b(?:api[_-]?key|secret[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|db[_-]?password|password)\b' + r'(?i)\b(?:api[_-]?key|secret[_-]?key|secretkey|access[_-]?key|access[_-]?secret|access[_-]?token|auth[_-]?token|client[_-]?secret|account[_-]?key|accountkey|db[_-]?password|password|pwd)\b' r'\s*[:=]\s*[\'"]([^\'"\s]{12,})[\'"]' ) @@ -86,6 +91,13 @@ def __init__(self, _void): DETECTION_TYPE_GENERIC_ASSIGNMENT = ( 'generic_assignment' # nosec B105 - detector type id, not a credential. ) + DETECTION_TYPE_AUTHORIZATION_BEARER = ( + 'authorization_bearer' # nosec B105 - detector type id, not a credential. + ) + CAPTURE_GROUP_RULES = ( + DETECTION_TYPE_GENERIC_ASSIGNMENT, + DETECTION_TYPE_AUTHORIZATION_BEARER, + ) SECRET_RULES = ( ('aws_access_key', AWS_ACCESS_KEY_RE, 95), @@ -93,8 +105,11 @@ def __init__(self, _void): ('private_key', PRIVATE_KEY_RE, 98), ('google_api_key', GOOGLE_API_KEY_RE, 90), ('github_token', GITHUB_TOKEN_RE, 95), + ('github_fine_grained_token', GITHUB_FINE_GRAINED_TOKEN_RE, 95), ('slack_token', SLACK_TOKEN_RE, 95), ('stripe_key', STRIPE_KEY_RE, 95), + ('square_token', SQUARE_TOKEN_RE, 92), + (DETECTION_TYPE_AUTHORIZATION_BEARER, AUTHORIZATION_BEARER_RE, 88), ('database_url', DB_URL_RE, 92), (DETECTION_TYPE_GENERIC_ASSIGNMENT, ASSIGNMENT_RE, 70), ) @@ -147,7 +162,7 @@ def detect(cls, text): for match in pattern.finditer(str(text or '')): value = ( match.group(1) - if secret_type == cls.DETECTION_TYPE_GENERIC_ASSIGNMENT and match.groups() + if secret_type in cls.CAPTURE_GROUP_RULES and match.groups() else match.group(0) ) if cls._is_probable_false_positive(secret_type, value): @@ -326,6 +341,8 @@ def _is_probable_false_positive(cls, secret_type, value): 'dummy', 'placeholder', 'changeme', + 'redacted', + 'masked', 'your_', 'insert_', '<', diff --git a/src/core/http/proxy.py b/src/core/http/proxy.py index 8082417a..71c24861 100644 --- a/src/core/http/proxy.py +++ b/src/core/http/proxy.py @@ -24,7 +24,7 @@ from urllib3 import ProxyManager, Timeout, disable_warnings from urllib3.util import make_headers -from urllib3.exceptions import DependencyWarning, MaxRetryError, ProxySchemeUnknown, ReadTimeoutError, InsecureRequestWarning +from urllib3.exceptions import DecodeError, DependencyWarning, MaxRetryError, ProxySchemeUnknown, ReadTimeoutError, InsecureRequestWarning from src.core import helper from .exceptions import ProxyRequestError @@ -301,6 +301,11 @@ def request(self, url, extra_headers=None): self.__finish_active_terminal_line() self.__tpl.warning(key='read_timeout_error', url=helper.parse_url(url).path) + except DecodeError: + if self.__is_directory_like_scan() is True: + self.__finish_active_terminal_line() + self.__tpl.warning(key='decode_error', url=helper.parse_url(url).path) + def __retry_after_max_retry(self, url, request_headers): """ Retry a proxy request once after MaxRetryError without leaking raw urllib3 tracebacks. @@ -318,6 +323,9 @@ def __retry_after_max_retry(self, url, request_headers): except ReadTimeoutError as error: self.__record_tls_transport_error(error) self.__warn_proxy_retry_failed(url, error) + except DecodeError: + self.__finish_active_terminal_line() + self.__tpl.warning(key='decode_error', url=helper.parse_url(url).path) return None diff --git a/src/core/http/tls.py b/src/core/http/tls.py index d87e1463..4ca6f533 100644 --- a/src/core/http/tls.py +++ b/src/core/http/tls.py @@ -75,6 +75,17 @@ def describe_tls_transport_error(error): message = _compact_error_message(error) lowered = message.lower() + name_resolution_markers = ( + 'failed to resolve', + 'name or service not known', + 'nodename nor servname provided', + 'temporary failure in name resolution', + 'no address associated with hostname', + 'nameresolutionerror', + ) + if any(marker in lowered for marker in name_resolution_markers): + return None + if 'dh key too small' in lowered or 'dh_key_too_small' in lowered: return ( 'TLS handshake failed: DH_KEY_TOO_SMALL. ' diff --git a/src/lib/browser/browser.py b/src/lib/browser/browser.py index c4290940..2a40e969 100644 --- a/src/lib/browser/browser.py +++ b/src/lib/browser/browser.py @@ -17,11 +17,13 @@ """ import copy +import re import socket as net_socket import threading import time import uuid from urllib.parse import unquote, urlsplit, urlunsplit +from html.parser import HTMLParser from .session import SessionManager, SessionError from src.core import HttpRequestError, HttpsRequestError, ProxyRequestError, ResponseError from src.core import FileSystemError @@ -54,6 +56,59 @@ class _WafGuardStop(Exception): """Internal sentinel used to stop wordlist streaming after WAF guard triggers.""" +class _VisibleTextExtractor(HTMLParser): + """Extract visible text while ignoring script/style-like blocks.""" + + HIDDEN_TAGS = {'script', 'style', 'noscript', 'template'} + + def __init__(self): + """Initialize the bounded visible text collector.""" + + HTMLParser.__init__(self, convert_charrefs=True) + self._hidden_depth = 0 + self._chunks = [] + + def handle_starttag(self, tag, _attrs): + """Track hidden HTML blocks. + + :param str tag: normalized start tag name + :param list _attrs: parsed tag attributes, unused + :return: None + """ + + if str(tag or '').lower() in self.HIDDEN_TAGS: + self._hidden_depth += 1 + + def handle_endtag(self, tag): + """Leave hidden HTML blocks. + + :param str tag: normalized end tag name + :return: None + """ + + if str(tag or '').lower() in self.HIDDEN_TAGS and self._hidden_depth > 0: + self._hidden_depth -= 1 + + def handle_data(self, data): + """Collect visible text chunks only. + + :param str data: text node content + :return: None + """ + + if self._hidden_depth <= 0: + self._chunks.append(str(data or '')) + + def text(self): + """Return normalized visible text. + + :return: whitespace-normalized visible text + :rtype: str + """ + + return ' '.join(' '.join(self._chunks).split()) + + class Browser(Filter): """ Browser class """ @@ -91,6 +146,8 @@ class Browser(Filter): WAF_SAFE_RETRY_AFTER_STATUSES = (429, 503) WAF_SAFE_RETRY_AFTER_HEADER = 'retry-after' DEFAULT_RETRIES_FAIL_STREAK = 10 + TRANSPORT_OUTAGE_PAUSE_MIN_STREAK = 5 + TRANSPORT_OUTAGE_PAUSE_MAX_STREAK = 10 WAF_SAFE_RECOVERY_STATUSES = ('success', 'redirect', 'auth', 'forbidden', 'bad', 'certificate') def __init__(self, params): @@ -153,7 +210,13 @@ def __init__(self, params): key='method_override', sniffers=', '.join(method_override_items) ) - self.__result = {'total': {}, 'items': {}, 'report_items': {}, 'filtered_items': []} + self.__result = { + 'total': {}, + 'items': {}, + 'report_items': {}, + 'filtered_items': [], + 'transport_failed': [], + } self.__visited_recursive = set() self.__queued_recursive = set() runtime_paths = self.__prepare_runtime_paths() @@ -654,6 +717,30 @@ def __format_diagnostics_bool(value): return 'enabled' if value is True else 'disabled' + @staticmethod + def __format_transport_diagnostics(payload): + """Format transport-failure diagnostics for the active scan mode. + + Directory scans use the consecutive exhausted-retry fail-streak as an + availability guard. Subdomain scans do not enforce that guard because + missing HTTP responses are normal enumeration misses. + + :param dict payload: runtime diagnostics payload + :return: formatted diagnostics value + :rtype: str + """ + + if payload.get('retries_fail_streak_enforced') is not True: + return 'subdomain misses {0}, fail-fast disabled'.format( + payload.get('transport_skipped'), + ) + + return 'exhausted transport paths {0}, fail streak {1}/{2}'.format( + payload.get('transport_skipped'), + payload.get('retries_fail_streak'), + payload.get('retries_fail_limit'), + ) + def __record_pre_request_skip(self): """Record one dictionary item skipped before an HTTP request was submitted. @@ -705,6 +792,7 @@ def __runtime_diagnostics_payload(self, status='completed'): 'remaining_seconds': remaining_seconds, 'retries_fail_streak': self.__safe_progress_int(getattr(self, '_Browser__transport_failure_streak', 0)), 'retries_fail_limit': self.__transport_failure_threshold(), + 'retries_fail_streak_enforced': self.__is_subdomains_scan() is not True, 'auto_calibration_enabled': getattr(self.__config, 'is_auto_calibrate', False) is True, 'calibrated_responses': self.__safe_progress_int(total_counter.get('calibrated', 0)), } @@ -770,8 +858,9 @@ def __format_runtime_diagnostics(self, status='completed'): ('items', total), ('progress', '{0}/{1} ({2})'.format(processed, total, progress)), ( - 'requests', - '{0} submitted, {1} skipped before request'.format( + 'queue', + '{0} consumed, {1} submitted, {2} pre-request skipped'.format( + payload.get('processed'), payload.get('submitted'), payload.get('skipped_before_request'), ), @@ -787,11 +876,7 @@ def __format_runtime_diagnostics(self, status='completed'): ('threads', payload.get('threads')), ( 'retries', - 'exhausted transport paths {0}, fail streak {1}/{2}'.format( - payload.get('transport_skipped'), - payload.get('retries_fail_streak'), - payload.get('retries_fail_limit'), - ), + self.__format_transport_diagnostics(payload), ), ( 'calibration', @@ -1160,7 +1245,7 @@ def fingerprint(self): evidence_values = self.__fingerprint_evidence_values(result.get('signals', [])) if evidence_values: - tpl.info(msg='Fingerprint evidence: {0}'.format(', '.join(evidence_values))) + tpl.debug(msg='Fingerprint evidence: {0}'.format(', '.join(evidence_values))) return result @@ -1203,24 +1288,31 @@ def calibrate(self): dns_wildcard_addresses = self.__build_dns_wildcard_addresses() signatures = [] - for url in self.__build_calibration_urls(): + calibration_urls = self.__build_calibration_urls() + calibration_probe_stats = { + 'blocked': 0, + 'failed': 0, + 'ignored': 0, + } + + for url in calibration_urls: try: response_object = self.__request_with_waf_safe_mode(url) response_data = self.__response.handle( response_object, request_url=url, items_size=0, - total_size=self.__config.calibration_samples, + total_size=len(calibration_urls), ignore_list=[], emit_debug=False ) if response_data is None: + calibration_probe_stats['ignored'] += 1 continue if response_data[0] == 'blocked': - tpl.warning( - msg='Auto-calibration probe skipped because it was classified as blocked: {0}'.format(url)) + calibration_probe_stats['blocked'] += 1 continue signatures.append(Calibration.build_signature(response_object, response_data)) @@ -1229,11 +1321,30 @@ def calibrate(self): if self.__is_standalone_proxy_mode() is True: raise BrowserError(error) + calibration_probe_stats['failed'] += 1 tpl.warning(msg='Auto-calibration probe failed: {0}'.format(error)) except (HttpRequestError, HttpsRequestError, ResponseError) as error: + calibration_probe_stats['failed'] += 1 tpl.warning(msg='Auto-calibration probe failed: {0}'.format(error)) + http_baseline_disabled = False + if self.__is_weak_calibration_baseline(len(signatures), len(calibration_urls)) is True: + http_baseline_disabled = True + tpl.warning( + msg=( + 'Auto-calibration HTTP baseline is weak: usable={0}/{1}, blocked={2}, failed={3}, ' + 'ignored={4}. Runtime response calibration disabled for this target.' + ).format( + len(signatures), + len(calibration_urls), + calibration_probe_stats['blocked'], + calibration_probe_stats['failed'], + calibration_probe_stats['ignored'], + ) + ) + signatures = [] + self.__calibration = Calibration( signatures=signatures, threshold=self.__config.calibration_threshold, @@ -1257,13 +1368,42 @@ def calibrate(self): self.__mark_session_dirty() return self.__calibration - tpl.warning(msg='Auto-calibration disabled: no usable baseline signatures') + if http_baseline_disabled is not True: + tpl.warning(msg='Auto-calibration disabled: no usable baseline signatures') return None except (AttributeError, TypeError, ValueError) as error: tpl.warning(msg='Auto-calibration skipped: {0}'.format(error)) return None + @staticmethod + def __is_weak_calibration_baseline(usable_signatures, requested_samples): + """Return True when the HTTP calibration baseline is too sparse. + + Weak baselines are dangerous because a single usable response can + overfit noisy targets after most probes were blocked, ignored, or + failed. Keep one-sample and two-sample scans backward-compatible, but + require a minimum half-sample quorum for larger calibration runs. + + :param int usable_signatures: number of usable response signatures + :param int requested_samples: number of requested calibration probes + :return: True when response calibration should be disabled + :rtype: bool + """ + + try: + usable_signatures = int(usable_signatures or 0) + requested_samples = int(requested_samples or 0) + except (TypeError, ValueError): + return False + + if requested_samples < 3 or usable_signatures <= 0: + return False + + minimum_usable = max(2, (requested_samples + 1) // 2) + + return usable_signatures < minimum_usable + def __is_active_sniffer_enabled(self, name): """Return True when a built-in active sniffer is enabled. @@ -1370,7 +1510,40 @@ def __build_calibration_urls(self): urls = [] token = uuid.uuid4().hex[:12] - path_templates = ( + path_templates = self.__build_calibration_path_templates() + + for index in range(self.__config.calibration_samples): + template = path_templates[index % len(path_templates)] + path = template.format(token=token, index=index) + urls.append(self.__build_calibration_url(path)) + + return urls + + def __build_calibration_path_templates(self): + """Return calibration path templates for the current runtime profile. + + Regular auto-calibration keeps mixed URL shapes to detect different + soft-404/catch-all behaviours. In WAF safe mode, avoid high-risk + scanner-like probes such as `.php`, `.map`, `admin`, and + `wp-content/uploads/*.php` because those paths are commonly treated as + attack indicators by edge protections before the scan starts. + + :return: tuple[str, ...] + """ + + if True is getattr(self.__config, 'is_waf_safe_mode', False): + return ( + '{token}-{index}', + 'assets/{token}-{index}', + 'static/{token}-{index}', + 'media/{token}-{index}', + 'content/{token}-{index}', + 'resources/{token}-{index}', + 'public/{token}-{index}', + 'files/{token}-{index}', + ) + + return ( '{token}-{index}', 'assets/{token}-{index}.map', '{token}-{index}.php', @@ -1381,13 +1554,6 @@ def __build_calibration_urls(self): 'admin/{token}-{index}', ) - for index in range(self.__config.calibration_samples): - template = path_templates[index % len(path_templates)] - path = template.format(token=token, index=index) - urls.append(self.__build_calibration_url(path)) - - return urls - def __build_calibration_url(self, path): """ Build a calibration URL under the configured scan prefix. @@ -2321,6 +2487,56 @@ def __transport_failures_skipped_count(self): with self.__transport_failure_lock: return int(getattr(self, '_Browser__transport_failures_skipped', 0)) + def __transport_outage_pause_threshold(self, abort_threshold=None): + """Return the conservative streak threshold for auto-pausing scans. + + The value is derived from the existing ``--retries-fail-streak`` guard so + patch releases do not add a new public option. Very low fail-fast limits + still abort normally before the pause threshold can fire. + + :param int|None abort_threshold: configured fail-streak abort threshold + :return: pause threshold + :rtype: int + """ + + if abort_threshold is None: + abort_threshold = self.__transport_failure_threshold() + + try: + abort_threshold = int(abort_threshold) + except (TypeError, ValueError): + abort_threshold = self.DEFAULT_RETRIES_FAIL_STREAK + + tenth = max(1, abort_threshold // 10) + return min( + self.TRANSPORT_OUTAGE_PAUSE_MAX_STREAK, + max(self.TRANSPORT_OUTAGE_PAUSE_MIN_STREAK, tenth), + ) + + def __request_transport_outage_pause(self, streak, threshold, path): + """Ask the thread pool to open the regular pause prompt on the main thread. + + :param int streak: consecutive transport failure count + :param int threshold: configured fail-streak abort threshold + :param str path: last failed path for diagnostics + :return: None + """ + + request_pause = getattr(getattr(self, '_Browser__pool', None), 'request_pause', None) + if not callable(request_pause): + return + + if request_pause() is not True: + return + + tpl.warning( + msg=( + 'Network outage suspected after {streak} consecutive transport failures. ' + 'Scan paused to avoid consuming more dictionary entries. Last failed path: {path}. ' + 'Limit: {threshold}.' + ).format(streak=streak, threshold=threshold, path=path) + ) + def __emit_transport_failure_summary(self): """Print a compact summary for skipped path-specific transport failures. @@ -2335,12 +2551,19 @@ def __emit_transport_failure_summary(self): return self.__finish_filtered_progress_line() - tpl.info( - msg=( - 'Transport failures skipped: {0} request(s) without HTTP response. ' - 'Scan continued without reaching --retries-fail-streak.' - ).format(skipped) - ) + + if self.__is_subdomains_scan() is True: + tpl.info( + msg='Skipped subdomain candidates without HTTP response: {0}.'.format(skipped) + ) + else: + tpl.info( + msg=( + 'Transport failures skipped: {0} request(s) without HTTP response. ' + 'Scan continued without reaching --retries-fail-streak.' + ).format(skipped) + ) + self.__transport_failure_summary_emitted = True def __reset_transport_failure_streak(self): @@ -2380,19 +2603,44 @@ def __record_transport_failure(self, url): Request providers already pass ``config.retries`` to urllib3. This method is called only after that retry budget has been exhausted and the - provider returned ``None``. It must not short-circuit per-request retry - behavior; it only protects the scanner from silently walking the whole - dictionary after the target transport goes away. + provider returned ``None``. Directory scans use a consecutive-failure + guard to avoid walking the remaining dictionary against an unavailable + target. Subdomain scans intentionally do not use that guard because + missing HTTP responses are normal enumeration misses. :param str url: failed request URL - :raise BrowserError: when consecutive failures exceed the abort threshold + :raise BrowserError: when directory-scan consecutive failures exceed the abort threshold :return: None """ + is_subdomains_scan = self.__is_subdomains_scan() is True + + self.__ensure_session_runtime_state() + with self.__transport_failure_lock: - self.__transport_failure_streak += 1 self.__transport_failures_skipped += 1 - streak = self.__transport_failure_streak + + if is_subdomains_scan: + self.__transport_failure_streak = 0 + streak = 0 + else: + self.__transport_failure_streak += 1 + streak = self.__transport_failure_streak + self.__result['transport_failed'].append({ + 'url': url, + 'size': '0B', + 'code': '-', + 'reason': 'no_response_after_retries', + }) + + if is_subdomains_scan: + self.__catch_report_data('ignored', url) + self.__emit_filtered_progress( + 'ignored', + ('ignored', url, '0B', '-'), + request_url=url, + ) + return threshold = self.__transport_failure_threshold() path = helper.parse_url(url).path or str(url) @@ -2416,6 +2664,10 @@ def __record_transport_failure(self, url): diagnostic_suffix=diagnostic_suffix, ) ) + + if streak >= self.__transport_outage_pause_threshold(threshold): + self.__request_transport_outage_pause(streak, threshold, path) + return raise BrowserError( @@ -2451,7 +2703,6 @@ def __http_request(self, url, depth=0): resp = self.__request_with_waf_safe_mode(url) if resp is None: - self.__catch_report_data('ignored', url) self.__record_transport_failure(url) return @@ -2513,6 +2764,20 @@ def __http_request(self, url, depth=0): metadata=calibration_match ) return + + js_cookie_challenge = self.__match_js_cookie_reload_challenge(resp, response_data) + if js_cookie_challenge is not None: + self.__emit_passive_sniffer_findings(passive_sniffer_findings, resp) + self.__emit_filtered_progress('calibrated', response_data, js_cookie_challenge, request_url=url) + self.__catch_report_data( + 'calibrated', + response_data[1], + response_data[2], + response_data[3], + metadata=js_cookie_challenge + ) + return + debug_response_data = getattr(self.__response, 'debug_response_data', None) if callable(debug_response_data) and primary_suppressed is not True: self.__clear_filtered_progress() @@ -2599,6 +2864,119 @@ def __should_suppress_primary_response(self, response_object, response_data, pas return False + @classmethod + def __match_js_cookie_reload_challenge(cls, response_object, response_data): + """ + Match JavaScript cookie bootstrap pages emitted as 2xx responses. + + Some hosting frontends return a small 200 HTML page that only sets a + browser cookie and reloads the current URL. Browsers execute the script + and then receive the real origin response, but raw scanners see the + bootstrap page as a false positive success. Treat only script-only + cookie+reload gates as filtered scan noise. + + :param object response_object: raw response object + :param tuple response_data: legacy classified response tuple + :return: calibration-like metadata when the response should be filtered + :rtype: dict|None + """ + + code = cls.__extract_response_code(response_object, response_data) + if code is None or code < 200 or code >= 300: + return None + + body = cls.__response_body_text(response_object) + if not body: + return None + + if len(body) > 2048: + return None + + content_type = str(cls.__get_header_value(response_object, 'content-type') or '').lower() + if content_type and 'html' not in content_type: + return None + + lowered = body.lower() + compact = re.sub(r'\s+', '', lowered) + + if 'document.cookie' not in compact: + return None + + if 'location.reload(' not in compact and 'window.location.reload(' not in compact: + return None + + if cls.__has_useful_html_controls(lowered) is True: + return None + + if cls.__visible_text_without_scripts(body): + return None + + known_cookie_gate = any(marker in compact for marker in ( + 'beget=begetok', + '__js_p_=', + )) + if known_cookie_gate is not True: + script_only = compact.startswith(']+>', ' ', str(body or '')) + value = re.sub(r'\s+', ' ', value) + return value.strip() + + return parser.text() + @staticmethod def __is_soft404_suppressor_response(response_object, primary_bucket, response_code): """ @@ -3839,8 +4217,12 @@ def __ensure_session_runtime_state(self): self.__result = { 'total': helper.counter(), 'items': helper.list(), - 'report_items': helper.list() + 'report_items': helper.list(), + 'transport_failed': [], } + + if isinstance(self.__result, dict) and not isinstance(self.__result.get('transport_failed'), list): + self.__result['transport_failed'] = [] if not hasattr(self, '_Browser__waf_safe_lock'): self.__waf_safe_lock = threading.RLock() diff --git a/src/lib/browser/calibration.py b/src/lib/browser/calibration.py index ff0a42a2..4c33351d 100644 --- a/src/lib/browser/calibration.py +++ b/src/lib/browser/calibration.py @@ -401,6 +401,10 @@ def _score(cls, baseline, candidate): reasons = [] if baseline.get('code') != candidate.get('code'): + cross_status_score, cross_status_reasons = cls._cross_status_soft_error_score(baseline, candidate) + if cross_status_score >= cls._cross_status_soft_error_min_score(): + return cross_status_score, cross_status_reasons + return 0.0, reasons score = 0.18 @@ -499,6 +503,207 @@ def _score(cls, baseline, candidate): return min(score, 1.0), reasons + @staticmethod + def _cross_status_soft_error_min_score(): + """ + Return the minimum score for cross-status soft-error suppression. + + This branch handles servers that occasionally return a 2xx status with a + canonical 404/410 body. The threshold is intentionally strict because it + bypasses the normal same-status calibration guard. + + :return: minimum score + :rtype: float + """ + + return 0.90 + + @staticmethod + def _is_soft_error_status_pair(baseline_code, candidate_code): + """ + Check whether a baseline/candidate status pair can represent a soft error. + + :param int|None baseline_code: calibration baseline status code + :param int|None candidate_code: candidate response status code + :return: True when a 4xx baseline can safely suppress a 2xx candidate + :rtype: bool + """ + + return baseline_code in (404, 410) and candidate_code in (200, 201, 202, 203, 204, 205, 206) + + @classmethod + def _has_strong_soft_error_semantics(cls, signature): + """ + Decide whether a signature looks like a canonical missing-resource page. + + :param dict signature: calibration signature + :return: True when title/visible text carries strong 404-style semantics + :rtype: bool + """ + + phrases = set(signature.get('semantic_phrases') or []) + title = str(signature.get('title') or '').strip().lower() + + if title in ('404', '404 not found', 'not found', 'page not found'): + return True + + if '404' in phrases and 'not found' in phrases: + return True + + if 'not found' in phrases and {'requested page', 'requested resource'} & phrases: + return True + + return False + + @classmethod + def _has_any_soft_error_semantics(cls, signature): + """ + Decide whether a signature carries at least a weak missing-resource marker. + + Some compact nginx/hosting 404 templates contain only a short ``Not Found`` + body without a title or ``requested URL`` wording. That is too weak by + itself, but it is useful when paired with an almost identical 4xx baseline. + + :param dict signature: response signature + :return: True when a weak 404-style marker is present + :rtype: bool + """ + + if cls._has_strong_soft_error_semantics(signature) is True: + return True + + phrases = set(signature.get('semantic_phrases') or []) + title = str(signature.get('title') or '').strip().lower() + + return title in ('404', '404 not found', 'not found') or bool({'404', 'not found'} & phrases) + + @classmethod + def _is_compact_cross_status_soft_error_shape_match(cls, baseline, candidate): + """ + Return True for compact 4xx error templates emitted with a transient 2xx. + + This is intentionally stricter than normal same-status calibration: it + requires a 404/410-to-2xx pair, weak missing-resource semantics on both + sides, small comparable bodies, compatible content kinds, and at least one + stable shape hash match. + + :param dict baseline: baseline calibration signature + :param dict candidate: candidate response signature + :return: True when the candidate is the same compact error template + :rtype: bool + """ + + if cls._is_soft_error_status_pair(baseline.get('code'), candidate.get('code')) is not True: + return False + + if cls._has_any_soft_error_semantics(baseline) is not True: + return False + + if cls._has_any_soft_error_semantics(candidate) is not True: + return False + + baseline_kind = baseline.get('content_kind') + candidate_kind = candidate.get('content_kind') + + if baseline_kind and candidate_kind: + if baseline_kind != candidate_kind or baseline_kind not in ('html', 'text'): + return False + + baseline_size = cls._soft_200_number(baseline.get('size')) + candidate_size = cls._soft_200_number(candidate.get('size')) + + if min(baseline_size, candidate_size) < 32: + return False + if max(baseline_size, candidate_size) > 1024: + return False + if cls._soft_200_similarity(baseline_size, candidate_size) < 0.965: + return False + + stable_hashes = ( + 'normalized_body_hash', + 'body_skeleton_hash', + 'visible_text_hash', + 'dom_token_hash', + ) + + for name in stable_hashes: + baseline_hash = baseline.get(name) + candidate_hash = candidate.get(name) + if baseline_hash and candidate_hash and baseline_hash == candidate_hash: + return True + + return False + + @classmethod + def _cross_status_soft_error_score(cls, baseline, candidate): + """ + Score a 2xx candidate against a canonical 4xx soft-error baseline. + + Normal calibration intentionally requires identical status codes. This + narrowly scoped exception catches origin/proxy inconsistencies where the + body is the same missing-resource page but the status was emitted as 2xx. + + :param dict baseline: calibration baseline signature + :param dict candidate: candidate response signature + :return: score and reasons + :rtype: tuple[float, list[str]] + """ + + reasons = [] + + if cls._is_soft_error_status_pair(baseline.get('code'), candidate.get('code')) is not True: + return 0.0, reasons + + compact_shape_match = cls._is_compact_cross_status_soft_error_shape_match(baseline, candidate) + + if ( + cls._has_strong_soft_error_semantics(baseline) is not True + or cls._has_strong_soft_error_semantics(candidate) is not True + ) and compact_shape_match is not True: + return 0.0, reasons + + score = 0.36 + reasons.append('cross-status-soft-error') + + if compact_shape_match is True: + score += 0.12 + reasons.append('compact-soft-error-shape') + + if baseline.get('normalized_body_hash') == candidate.get('normalized_body_hash'): + score += 0.22 + reasons.append('body-hash') + + if baseline.get('body_skeleton_hash') == candidate.get('body_skeleton_hash'): + score += 0.16 + reasons.append('skeleton-hash') + + if baseline.get('visible_text_hash') and baseline.get('visible_text_hash') == candidate.get('visible_text_hash'): + score += 0.18 + reasons.append('visible-text') + + phrase_score = cls._jaccard_similarity( + baseline.get('semantic_phrases') or [], + candidate.get('semantic_phrases') or [] + ) + if phrase_score >= 0.80: + score += 0.08 * phrase_score + reasons.append('semantic-phrases') + + dom_score = cls._sequence_similarity( + baseline.get('dom_tokens') or [], + candidate.get('dom_tokens') or [] + ) + if dom_score >= 0.90: + score += 0.05 * dom_score + reasons.append('dom-structure') + + size_score = cls._numeric_similarity(baseline.get('size'), candidate.get('size')) + if size_score >= 0.95: + score += 0.05 * size_score + reasons.append('size') + + return min(score, 1.0), reasons + @staticmethod def _hash(value): """ diff --git a/src/lib/browser/debug.py b/src/lib/browser/debug.py index 82caf055..ad459d67 100644 --- a/src/lib/browser/debug.py +++ b/src/lib/browser/debug.py @@ -285,6 +285,12 @@ def debug_request_uri(self, status, request_uri, **kwargs): tpl.line(msg='Malware', color='red'), tpl.line(msg=urlpath, color='green') ) + elif status in ['secret']: + request_uri = '{0} ({1}) {2}'.format( + tpl.line(msg='OK', color='green'), + tpl.line(msg='Secret', color='red'), + tpl.line(msg=urlpath, color='green') + ) elif status in ['stacktrace']: request_uri = '{0} ({1}) {2}'.format( tpl.line(msg='OK', color='green'), @@ -323,7 +329,7 @@ def debug_request_uri(self, status, request_uri, **kwargs): self.__clear = True if self.__catched else False - if status in ['success', 'file', 'bad', 'forbidden', 'redirect', 'blocked', 'indexof', 'certificate', 'auth', 'stacktrace', 'malware', 'shadow', 'openredirect']: + if status in ['success', 'file', 'bad', 'forbidden', 'redirect', 'blocked', 'indexof', 'certificate', 'auth', 'stacktrace', 'secret', 'malware', 'shadow', 'openredirect']: sys.writels('', flush=True) tpl.info( key='get_item', diff --git a/src/lib/browser/fingerprint.py b/src/lib/browser/fingerprint.py index a03108c4..a2cfd104 100644 --- a/src/lib/browser/fingerprint.py +++ b/src/lib/browser/fingerprint.py @@ -138,7 +138,7 @@ def _default_result(cls): STATIC_CATEGORY = 'static' RUNTIME_CATEGORY = 'runtime' TECHNOLOGY_RUNTIME_MAP = { - 'WordPress': 'PHP', 'WooCommerce': 'PHP', 'Drupal': 'PHP', 'Joomla': 'PHP', 'Magento': 'PHP', 'Bitrix': 'PHP', 'OpenCart': 'PHP', 'PrestaShop': 'PHP', 'TYPO3': 'PHP', 'Nextcloud': 'PHP', 'ownCloud': 'PHP', 'Matomo': 'PHP', 'phpMyAdmin': 'PHP', 'phpBB': 'PHP', 'Moodle': 'PHP', 'Open Journal Systems': 'PHP', 'InstantCMS': 'PHP', 'DiafanCMS': 'PHP', 'Laravel': 'PHP', 'Symfony': 'PHP', 'Craft CMS': 'PHP', 'Bolt CMS': 'PHP', 'RoundCube Webmail': 'PHP', 'WHMCS': 'PHP', 'CS-Cart': 'PHP', 'CubeCart': 'PHP', 'DataLife Engine': 'PHP', 'Discuz!': 'PHP', 'SilverStripe': 'PHP', 'Webasyst / Shop-Script': 'PHP', 'XOOPS': 'PHP', 'Zen Cart CMS': 'PHP', 'e107': 'PHP', 'phpWind': 'PHP', 'phpCMS': 'PHP', + 'WordPress': 'PHP', 'WooCommerce': 'PHP', 'Drupal': 'PHP', 'Joomla': 'PHP', 'Magento': 'PHP', 'Bitrix': 'PHP', 'OpenCart': 'PHP', 'PrestaShop': 'PHP', 'TYPO3': 'PHP', 'Nextcloud': 'PHP', 'ownCloud': 'PHP', 'Matomo': 'PHP', 'phpMyAdmin': 'PHP', 'phpBB': 'PHP', 'Moodle': 'PHP', 'Open Journal Systems': 'PHP', 'Evolution CMS': 'PHP', 'MogutaCMS': 'PHP', 'CMS.S3 / Megagroup': 'PHP', 'Camaleon CMS': 'Ruby', 'Melbis Shop Platform': 'PHP', 'InstantCMS': 'PHP', 'DiafanCMS': 'PHP', 'Laravel': 'PHP', 'Symfony': 'PHP', 'Craft CMS': 'PHP', 'Bolt CMS': 'PHP', 'RoundCube Webmail': 'PHP', 'WHMCS': 'PHP', 'CS-Cart': 'PHP', 'CubeCart': 'PHP', 'DataLife Engine': 'PHP', 'Discuz!': 'PHP', 'SilverStripe': 'PHP', 'Webasyst / Shop-Script': 'PHP', 'XOOPS': 'PHP', 'Zen Cart CMS': 'PHP', 'e107': 'PHP', 'phpWind': 'PHP', 'phpCMS': 'PHP', 'Express': 'Node.js', 'NestJS': 'Node.js', 'Fastify': 'Node.js', 'Koa': 'Node.js', 'Hapi': 'Node.js', 'Strapi': 'Node.js', 'Directus': 'Node.js', 'Ghost': 'Node.js', 'Next.js': 'Node.js', 'Nuxt': 'Node.js', 'Gatsby': 'Node.js', 'Astro': 'Node.js', 'Remix': 'Node.js', 'SvelteKit': 'Node.js', 'Docusaurus': 'Node.js', 'VitePress': 'Node.js', 'PencilBlue': 'Node.js', 'React': 'JavaScript', 'Vue': 'JavaScript', 'Angular': 'JavaScript', 'Django': 'Python', 'Flask': 'Python', 'FastAPI': 'Python', 'Ruby on Rails': 'Ruby', 'Spree': 'Ruby', 'Spring': 'Java/JVM', 'Liferay': 'Java/JVM', 'OpenCms': 'Java/JVM', 'Hippo CMS': 'Java/JVM', 'dotCMS': 'Java/JVM', 'ASP.NET': '.NET', 'Microsoft SharePoint': '.NET', 'DNN Platform': '.NET', 'Orchard CMS': '.NET', 'Sitecore': '.NET', 'Sitefinity': '.NET', 'Umbraco': '.NET', 'Phoenix': 'Elixir', 'MkDocs': 'Static site', 'Jekyll': 'Static site', 'Hugo': 'Static site', 'AsciiDoc': 'Static site', } @@ -158,6 +158,8 @@ def _default_result(cls): ('Bubble', SITE_BUILDER_CATEGORY, ('bubble', 'bubble.io')), ('CKAN', CMS_CATEGORY, ('ckan',)), ('CMS.S3 / Megagroup', CMS_CATEGORY, ('cms.s3', 'cms s3', 'megagroup cms')), + ('Camaleon CMS', CMS_CATEGORY, ('camaleon cms', 'camaleoncms')), + ('Melbis Shop Platform', ECOMMERCE_CATEGORY, ('melbis shop platform', 'melbis shop')), ('CMS Made Simple', CMS_CATEGORY, ('cms made simple', 'cmsms')), ('CMS CONTENIDO', CMS_CATEGORY, ('contenido', 'cms contenido')), ('CMSimple', CMS_CATEGORY, ('cmsimple',)), @@ -173,6 +175,7 @@ def _default_result(cls): ('EC-CUBE', ECOMMERCE_CATEGORY, ('ec-cube', 'eccube')), ('EPiServer', CMS_CATEGORY, ('episerver', 'optimizely cms')), ('ExpressionEngine', CMS_CATEGORY, ('expressionengine', 'expression engine')), + ('Evolution CMS', CMS_CATEGORY, ('evolution cms', 'evolutioncms', 'modx evolution')), ('Fork CMS', CMS_CATEGORY, ('fork cms',)), ('GetSimple CMS', CMS_CATEGORY, ('getsimple cms', 'get-simple cms')), ('GoDaddy Website Builder', SITE_BUILDER_CATEGORY, ('godaddy website builder', 'go central')), @@ -247,6 +250,16 @@ def _default_result(cls): ('DNN Platform', CMS_CATEGORY, ('__dnnvariable', 'dnn_', '/portals/_default/')), ('EC-CUBE', ECOMMERCE_CATEGORY, ('eccube', 'ec-cube', '/user_data/packages/')), ('ExpressionEngine', CMS_CATEGORY, ('expressionengine', 'exp:channel', 'powered by expressionengine')), + ( + 'Evolution CMS', + CMS_CATEGORY, + ( + 'powered by evolution cms', + 'evolution cms is not currently installed', + 'please run the evolution cms install utility', + 'modx evolution', + ), + ), ('GetSimple CMS', CMS_CATEGORY, ('getsimple', 'get-simple', '/data/uploads/')), ('GoDaddy Website Builder', SITE_BUILDER_CATEGORY, ('wsimg.com', 'godaddy.com/websites/website-builder')), ('Hostinger Website Builder', SITE_BUILDER_CATEGORY, ('hostinger website builder', 'userapp.zyrosite.com', 'assets.zyrosite.com', 'zyrosite.com')), @@ -306,6 +319,8 @@ def _default_result(cls): ('Salesforce Commerce Cloud', ECOMMERCE_CATEGORY, 'x-dw-request-base-id', None), ('Salesforce Commerce Cloud', ECOMMERCE_CATEGORY, 'x-dw-trace-id', None), ('AEM', CMS_CATEGORY, 'x-dispatcher', None), + ('UMI.CMS', CMS_CATEGORY, 'x-generated-by', 'umi.cms'), + ('UMI.CMS', CMS_CATEGORY, 'x-generated-by', 'umi cms'), ) EXTENDED_CMS_COOKIE_SIGNATURES = ( @@ -1383,6 +1398,497 @@ def _apply_dotcms_rules(self, body_lower, headers, cookies, probe_signals=None): elif len(found_markers) == 1 and len(found_cookies) >= 1: self._add_signal('dotCMS', self.CMS_CATEGORY, 'markup+cookie', '{0}+{1}'.format(found_markers[0], found_cookies[0]), 7) + + @classmethod + def _contains_mogutacms_brand(cls, text): + """ + Return True when text contains an explicit MogutaCMS brand marker. + + The matcher intentionally avoids a standalone "moguta" token because + portfolio pages, comparison articles and vendor links can mention the + product without proving that the scanned target runs on it. + + :param str text: normalized text + :return: check result + :rtype: bool + """ + + source = str(text or '').lower() + return re.search(r'(?= 2: + self._add_signal('MogutaCMS', self.ECOMMERCE_CATEGORY, 'asset', '+'.join(found_markers[:3]), 8) + elif len(found_markers) == 1 and (has_generator or has_powered_by): + self._add_signal('MogutaCMS', self.ECOMMERCE_CATEGORY, 'asset+brand', found_markers[0], 4) + + + def _apply_evolution_cms_rules(self, body_lower, generator, probe_statuses, not_found_status): + """ + Apply strong Evolution CMS signals. + + Evolution CMS descends from MODX Evolution, but short words like + "evo" or the generic /manager/ path are too noisy for standalone + detection. Keep the rule limited to explicit branding or core fallback + text, and use endpoint reachability only as corroborating evidence. + + :param str body_lower: normalized response body + :param str generator: raw generator meta value + :param dict probe_statuses: fingerprint endpoint probe statuses + :param int not_found_status: neutral 404-baseline status + :return: None + """ + + generator_lower = str(generator or '').lower() + explicit_markers = ( + 'evolution cms', + 'evolutioncms', + 'modx evolution', + ) + + has_explicit_marker = any(marker in generator_lower or marker in body_lower for marker in explicit_markers) + + if any(marker in generator_lower for marker in explicit_markers): + self._add_signal('Evolution CMS', self.CMS_CATEGORY, 'meta', 'generator={0}'.format(generator), 8) + + if 'powered by evolution cms' in body_lower: + self._add_signal('Evolution CMS', self.CMS_CATEGORY, 'markup', 'powered by evolution cms', 7) + + if ( + 'evolution cms is not currently installed' in body_lower + or 'please run the evolution cms install utility' in body_lower + ): + self._add_signal('Evolution CMS', self.CMS_CATEGORY, 'markup', 'install fallback', 8) + + if 'modx evolution' in body_lower: + self._add_signal('Evolution CMS', self.CMS_CATEGORY, 'markup', 'modx evolution', 7) + + if has_explicit_marker and self._is_distinct_probe_up( + probe_statuses, + '/manager/', + [200, 301, 302, 401, 403], + not_found_status, + ): + self._add_signal('Evolution CMS', self.CMS_CATEGORY, 'endpoint', '/manager/', 3) + + + @classmethod + def _collect_melbis_shop_signals(cls, body_lower, cookies, generator): + """ + Return conservative Melbis Shop Platform passive signals. + + Melbis Shop pages often expose a product footer such as + ``Powered by Melbis Shop v6.x`` or the older Russian + ``Магазин создан на базе Melbis Shop v5.x`` text. The ``MS_MSS`` + session cookie is useful corroboration, but it is intentionally not + accepted as standalone evidence. + + :param str body_lower: normalized response body + :param list cookies: normalized response cookie names + :param str generator: raw generator meta value + :return: detected signal tuples + :rtype: list[tuple[str, str]] + """ + + body_text = str(body_lower or '').lower() + generator_lower = str(generator or '').lower() + cookie_names = [str(cookie or '').lower() for cookie in cookies] + signals = [] + seen_values = set() + + def add_signal(signal_type, value): + if value in seen_values: + return + signals.append((signal_type, value)) + seen_values.add(value) + + if 'melbis shop' in generator_lower: + add_signal('meta', 'generator=Melbis Shop') + + if re.search(r'powered\s+by(?:\s|<[^>]+>)*melbis\s+shop(?:\s*v?[0-9][0-9a-z.\-]*)?', body_text): + add_signal('powered', 'Powered by Melbis Shop') + + if re.search(r'магазин\s+создан\s+на\s+базе\s+melbis\s+shop(?:\s*v?[0-9][0-9a-z.\-]*)?', body_text): + add_signal('powered', 'Магазин создан на базе Melbis Shop') + + asset_markers = ( + ('/templates/default/melbis.css', 'templates/default/melbis.css'), + ('/templates/default/melbis.js', 'templates/default/melbis.js'), + ('templates/default/melbis.css', 'templates/default/melbis.css'), + ('templates/default/melbis.js', 'templates/default/melbis.js'), + ) + for marker, value in asset_markers: + if marker in body_text: + add_signal('asset', value) + + source_markers = ( + ("definesession('melbis_shop')", "DefineSession('MELBIS_SHOP')"), + ('definesession("melbis_shop")', 'DefineSession("MELBIS_SHOP")'), + ('melbis()->defineselfconst', 'MELBIS()->DefineSelfConst'), + ('melbis_base_page', 'melbis_base_page'), + ) + for marker, value in source_markers: + if marker in body_text: + add_signal('source', value) + + if re.search(r'(?:href|src|action)=["\'][^"\']*(?:dir|goods|news)\.php\?id=', body_text): + add_signal('route', 'dir/goods/news.php?id') + elif re.search(r'\b(?:dir|goods|news)\.php\?id=', body_text): + add_signal('route', 'dir/goods/news.php?id') + + if 'ms_mss' in cookie_names: + add_signal('cookie', 'MS_MSS') + + return signals + + def _apply_melbis_shop_rules(self, body_lower, cookies, generator): + """ + Apply conservative passive Melbis Shop Platform fingerprint signals. + + Product footer, generator/source, and default template assets are strong + enough to classify Melbis Shop directly. The ``MS_MSS`` cookie is only + used with route/template corroboration to avoid cookie-name false + positives on unrelated PHP applications. + + :param str body_lower: normalized response body + :param list cookies: normalized response cookie names + :param str generator: raw generator meta value + :return: None + """ + + signals = self._collect_melbis_shop_signals(body_lower, cookies, generator) + if len(signals) <= 0: + return + + strong_values = [ + value for signal_type, value in signals + if signal_type in ('meta', 'powered', 'asset', 'source') + ] + route_values = [value for signal_type, value in signals if signal_type == 'route'] + cookie_values = [value for signal_type, value in signals if signal_type == 'cookie'] + + if len(strong_values) > 0: + self._add_signal( + 'Melbis Shop Platform', + self.ECOMMERCE_CATEGORY, + 'markup', + '+'.join(strong_values[:4]), + 9, + ) + if len(cookie_values) > 0: + self._add_signal('Melbis Shop Platform', self.ECOMMERCE_CATEGORY, 'cookie', cookie_values[0], 3) + if len(route_values) > 0: + self._add_signal('Melbis Shop Platform', self.ECOMMERCE_CATEGORY, 'route', route_values[0], 3) + elif len(cookie_values) > 0 and len(route_values) > 0: + self._add_signal( + 'Melbis Shop Platform', + self.ECOMMERCE_CATEGORY, + 'cookie+route', + '{0}+{1}'.format(cookie_values[0], route_values[0]), + 8, + ) + + + @classmethod + def _collect_camaleon_cms_signals(cls, body_lower, cookies, generator): + """ + Return conservative Camaleon CMS passive signals. + + Camaleon CMS is a Ruby on Rails CMS. Strong public markers include + Camaleon asset names used by the admin layout, generated Camaleon + image paths, Camaleon cookies observed by authenticated flows, and + explicit Camaleon branding paired with Rails CSRF metadata. Plain + marketing text alone is intentionally weak to avoid classifying blog + posts or documentation pages as Camaleon installations. + + :param str body_lower: normalized response body + :param list cookies: normalized response cookie names + :param str generator: raw generator meta value + :return: detected signal tuples + :rtype: list[tuple[str, str]] + """ + + body_text = str(body_lower or '').lower() + generator_lower = str(generator or '').lower() + cookie_names = [str(cookie or '').lower() for cookie in cookies] + signals = [] + seen_values = set() + + def add_signal(signal_type, value): + if value in seen_values: + return + signals.append((signal_type, value)) + seen_values.add(value) + + if 'camaleon cms' in generator_lower or 'camaleoncms' in generator_lower: + add_signal('meta', 'generator=Camaleon CMS') + + asset_markers = ( + ('camaleon_cms/admin/admin-basic-manifest', 'camaleon_cms/admin/admin-basic-manifest'), + ('/assets/camaleon_cms/', '/assets/camaleon_cms/'), + ('camaleon_cms/camaleon.png', 'camaleon_cms/camaleon.png'), + ('/camaleon_cms/', '/camaleon_cms/'), + ) + for marker, value in asset_markers: + if marker in body_text: + add_signal('asset', value) + + if 'auth_token' in cookie_names: + add_signal('cookie', 'auth_token') + if '_cms_session' in cookie_names: + add_signal('cookie', '_cms_session') + + brand_count = body_text.count('camaleon cms') + body_text.count('camaleoncms') + has_rails_csrf = cls._has_rails_authenticity_token_meta(body_text) + has_camaleon_public_context = bool( + 'camaleon.website/store/' in body_text + or 'camaleon.website/documentation/' in body_text + or 'camaleon cms, rails cms' in body_text + or 'camaleoncms. all rights reserved' in body_text + or 'built with ruby-on-rails' in body_text + ) + if brand_count >= 1 and has_rails_csrf and has_camaleon_public_context: + add_signal('markup', 'Camaleon CMS+Rails CSRF') + elif brand_count >= 2 and has_rails_csrf: + add_signal('markup', 'Camaleon CMS repeated+Rails CSRF') + + return signals + + def _apply_camaleon_cms_rules(self, body_lower, cookies, generator): + """ + Apply conservative passive Camaleon CMS fingerprint signals. + + This rule consumes only already fetched root response metadata. It does + not probe /admin or any Camaleon-specific endpoint. Strong asset/meta + signals are accepted directly; public brand text requires Rails CSRF + corroboration to avoid false positives on unrelated documentation. + + :param str body_lower: normalized response body + :param list cookies: normalized response cookie names + :param str generator: raw generator meta value + :return: None + """ + + signals = self._collect_camaleon_cms_signals(body_lower, cookies, generator) + if len(signals) <= 0: + return + + strong_values = [ + value for signal_type, value in signals + if signal_type in ('meta', 'asset', 'markup') + ] + cookie_values = [value for signal_type, value in signals if signal_type == 'cookie'] + + if len(strong_values) > 0: + self._add_signal( + 'Camaleon CMS', + self.CMS_CATEGORY, + 'markup', + '+'.join(strong_values[:4]), + 9, + ) + if len(cookie_values) > 0: + self._add_signal('Camaleon CMS', self.CMS_CATEGORY, 'cookie', '+'.join(cookie_values[:2]), 3) + elif len(cookie_values) >= 2: + self._add_signal('Camaleon CMS', self.CMS_CATEGORY, 'cookie', '+'.join(cookie_values[:2]), 7) + + + @classmethod + def _collect_cms_s3_root_signals(cls, body_lower): + """ + Return conservative CMS.S3 / Megagroup root-page signals. + + CMS.S3 pages commonly expose generated builder markup and runtime + assets such as ``/my/s3/``, ``/shared/s3/``, ``$ite.start(...)`` and + ``widget-type-*``. A single vendor footer/link is intentionally weak + and is not enough for standalone detection. + + :param str body_lower: normalized response body + :return: detected signal tuples + :rtype: list[tuple[str, str]] + """ + + body_text = str(body_lower or '').lower() + marker_rules = ( + ('/my/s3/', 'asset', '/my/s3/'), + ('/shared/s3/', 'asset', '/shared/s3/'), + ('/g/s3/', 'asset', '/g/s3/'), + ('/my/s3/xapi/public/', 'asset', '/my/s3/xapi/public/'), + ('$ite.start', 'script', '$ite.start'), + ('s3solutionspanel', 'script', 'S3SolutionsPanel'), + ('wm-widget-', 'markup', 'wm-widget-*'), + ('widget-type-', 'markup', 'widget-type-*'), + ('editorelement layer-type', 'markup', 'editorElement layer-type'), + ('data-api-type="popup-form"', 'markup', 'data-api-type=popup-form'), + ("data-api-type='popup-form'", 'markup', 'data-api-type=popup-form'), + ('megagroup.ru', 'vendor', 'megagroup.ru'), + ('mega-copyright', 'vendor', 'mega-copyright'), + ) + + signals = [] + seen_values = set() + for marker, signal_type, value in marker_rules: + if marker not in body_text or value in seen_values: + continue + signals.append((signal_type, value)) + seen_values.add(value) + + return signals + + def _apply_cms_s3_rules(self, body_lower): + """ + Apply conservative passive CMS.S3 / Megagroup root-page signals. + + Generic footer/vendor mentions remain weak catalog evidence. Strong + CMS.S3 classification requires multiple generated S3 builder/runtime + markers from the root page and does not depend on endpoint probes. + + :param str body_lower: normalized response body + :return: None + """ + + signals = self._collect_cms_s3_root_signals(body_lower) + if len(signals) <= 0: + return + + structural_signals = [ + value for signal_type, value in signals + if signal_type in ('asset', 'script', 'markup') + ] + vendor_signals = [value for signal_type, value in signals if signal_type == 'vendor'] + + if len(structural_signals) >= 2: + self._add_signal( + 'CMS.S3 / Megagroup', + self.CMS_CATEGORY, + 'markup', + '+'.join(structural_signals[:4]), + 9, + ) + if len(vendor_signals) > 0: + self._add_signal( + 'CMS.S3 / Megagroup', + self.CMS_CATEGORY, + 'vendor', + '+'.join(vendor_signals[:2]), + 3, + ) + + + def _apply_datalife_engine_rules(self, body_lower, generator): + """ + Apply conservative passive DataLife Engine (DLE) signals. + + DLE installations often remove explicit generator branding, but still + expose stable runtime globals and engine asset bundle paths such as + ``dle_root`` / ``dle_login_hash`` and ``engine/classes/js/dle_js.js``. + Keep short ``dle`` text out of body matching to avoid false positives. + + :param str body_lower: normalized response body + :param str generator: raw generator meta value + :return: None + """ + + body_text = str(body_lower or '').lower() + generator_lower = str(generator or '').lower() + brand_markers = ( + 'datalife engine', + 'data life engine', + 'dle-news.ru', + 'dle-news.com', + 'softnews media group', + ) + + if any(marker in generator_lower for marker in brand_markers): + self._add_signal('DataLife Engine', self.CMS_CATEGORY, 'meta', 'generator={0}'.format(generator), 8) + + if any(marker in body_text for marker in brand_markers): + self._add_signal('DataLife Engine', self.CMS_CATEGORY, 'markup', 'DataLife Engine branding', 8) + + dle_globals = sorted(set(re.findall( + r'(?:var\s+|window\.)(allow_dle_delete_news|dle_(?:root|admin|login_hash|group|skin|wysiwyg|act_lang|user_id|search_delay|search_value))\b', + body_text, + ))) + if len(dle_globals) >= 2: + self._add_signal('DataLife Engine', self.CMS_CATEGORY, 'script', '+'.join(dle_globals[:4]), 8) + elif len(dle_globals) == 1 and any(marker in body_text for marker in brand_markers): + self._add_signal('DataLife Engine', self.CMS_CATEGORY, 'script+brand', dle_globals[0], 4) + + has_dle_js_asset = '/engine/classes/js/dle_js.js' in body_text or 'engine/classes/js/dle_js.js' in body_text + has_dle_min_asset = '/engine/classes/min/index.php' in body_text or 'engine/classes/min/index.php' in body_text + if has_dle_js_asset: + self._add_signal('DataLife Engine', self.CMS_CATEGORY, 'asset', 'engine/classes/js/dle_js.js', 8) + elif has_dle_min_asset and (len(dle_globals) >= 1 or any(marker in body_text for marker in brand_markers)): + self._add_signal('DataLife Engine', self.CMS_CATEGORY, 'asset+script', 'engine/classes/min/index.php', 5) + + ajax_markers = ( + 'dle_root+"engine/ajax/', + "dle_root+'engine/ajax/", + 'dle_root + "engine/ajax/', + "dle_root + 'engine/ajax/", + '/engine/ajax/controller.php?mod=', + 'engine/ajax/controller.php?mod=', + ) + if any(marker in body_text for marker in ajax_markers) and (len(dle_globals) >= 1 or has_dle_js_asset): + self._add_signal('DataLife Engine', self.CMS_CATEGORY, 'ajax', 'engine/ajax/', 5) + def _apply_extended_cms_catalog_rules(self, body_lower, headers, cookies, generator): """ Apply extended catalog signals for CMSs not covered by dedicated rules. @@ -1479,6 +1985,112 @@ def _has_php_route_marker(body_lower, final_root_url): body_text, ) is not None + @staticmethod + def _has_rails_authenticity_token_meta(body_lower): + """ + Return True for canonical Rails CSRF meta tags. + + Rails commonly emits a csrf-param meta tag with the fixed + authenticity_token value together with csrf-token. Keep this more + specific than a generic csrf-token match to avoid false positives on + other frameworks. + + :param str body_lower: normalized response body + :return: check result + :rtype: bool + """ + + body_text = str(body_lower or '') + if 'csrf-token' not in body_text: + return False + + return ( + re.search( + r']+name=["\']csrf-param["\'][^>]+content=["\']authenticity_token["\']', + body_text, + ) is not None + or re.search( + r']+content=["\']authenticity_token["\'][^>]+name=["\']csrf-param["\']', + body_text, + ) is not None + ) + + @staticmethod + def _has_rails_ujs_marker(body_lower): + """ + Return True for Rails UJS/Turbo integration markers. + + These markers are not used as standalone Rails evidence. They only + strengthen an existing Rails CSRF/cookie hint. + + :param str body_lower: normalized response body + :return: check result + :rtype: bool + """ + + body_text = str(body_lower or '') + markers = ( + '@rails/ujs', + 'rails-ujs', + 'turbo-rails', + 'data-turbo-track=', + 'data-disable-with=', + 'data-remote="true"', + "data-remote='true'", + 'data-method="delete"', + 'data-method="patch"', + 'data-method="put"', + "data-method='delete'", + "data-method='patch'", + "data-method='put'", + ) + return any(marker in body_text for marker in markers) + + @staticmethod + def _has_rails_asset_marker(body_lower): + """ + Return True for Rails asset pipeline or Webpacker application assets. + + Generic application.js/application.css names are intentionally ignored. + A digest-like asset name is required and the result is only used with + Rails corroboration. + + :param str body_lower: normalized response body + :return: check result + :rtype: bool + """ + + body_text = str(body_lower or '') + return ( + re.search(r'/assets/application-[a-z0-9]{8,}\.(?:css|js)(?:[?"\']|$)', body_text) is not None + or re.search(r'/packs/(?:js|css)/application-[a-z0-9]{8,}\.(?:css|js)(?:[?"\']|$)', body_text) is not None + or re.search(r'/assets/manifest-[a-z0-9]{8,}\.json(?:[?"\']|$)', body_text) is not None + ) + + @staticmethod + def _has_rails_error_marker(body_lower): + """ + Return True for exposed Rails exception or diagnostic markers. + + These strings are framework-specific and only consume response bodies + already fetched by fingerprinting. No active error probing is added. + + :param str body_lower: normalized response body + :return: check result + :rtype: bool + """ + + body_text = str(body_lower or '') + error_markers = ( + 'actioncontroller::routingerror', + 'actioncontroller::unknownformat', + 'actionview::template::error', + 'activerecord::', + 'rails.root', + 'action dispatch', + ) + return any(marker in body_text for marker in error_markers) + @staticmethod def _looks_like_express_not_found(not_found_status, not_found_body_lower): """ @@ -1592,6 +2204,46 @@ def _is_distinct_probe_up(cls, probe_statuses, path, allowed_statuses, not_found status = probe_statuses.get(path) return status in allowed_statuses and cls._is_distinct_probe_status(status, not_found_status) + @classmethod + def _is_wordpress_probe_up( + cls, + probe_statuses, + path, + not_found_status, + has_root_evidence, + public_statuses, + corroborated_statuses, + ): + """ + Return True when a WordPress endpoint probe is safe to use as evidence. + + WordPress static paths often return 403 on real installations, but + many non-WordPress hosts also return generic 403/405 deny templates + for suspicious paths. Endpoint-only restricted statuses therefore need + root-page corroboration before they can become WordPress evidence. + + :param dict probe_statuses: collected probe statuses + :param str path: probe path + :param int|None not_found_status: missing-path baseline HTTP status + :param bool has_root_evidence: whether the root response exposed WordPress markers + :param list[int] public_statuses: statuses accepted without root corroboration + :param list[int] corroborated_statuses: statuses accepted only with root corroboration + :return: bool + """ + + try: + status = int(probe_statuses.get(path) or 0) + except (TypeError, ValueError): + return False + + if status <= 0 or cls._is_distinct_probe_status(status, not_found_status) is not True: + return False + + if status in public_statuses: + return True + + return bool(has_root_evidence and status in corroborated_statuses) + @staticmethod def _should_propagate_runtime_from_signal(signal_type): """ @@ -1649,7 +2301,19 @@ def _apply_detection_rules( x_amz_cf_id = str(headers.get('x-amz-cf-id', '')).lower() x_amz_request_id = str(headers.get('x-amz-request-id', '')).lower() x_amz_id_2 = str(headers.get('x-amz-id-2', '')).lower() + content_security_policy = str(headers.get('content-security-policy', '')).lower() + surrogate_key = str(headers.get('surrogate-key', '')).lower() + x_wf_region = str(headers.get('x-wf-region', '')).lower() final_root_lower = str(final_root_url).lower() + host_lower = str(getattr(self.__config, 'host', '') or '').lower() + webflow_hosted_context = bool( + host_lower == 'webflow.io' + or host_lower.endswith('.webflow.io') + or x_wf_region + or 'webflow.io' in surrogate_key + or 'webflow.com' in content_security_policy + or 'webflow.io' in content_security_policy + ) not_found_body_lower = str(not_found_body).lower() not_found_powered_by = str(not_found_headers.get('x-powered-by', '')).lower() not_found_server = str(not_found_headers.get('server', '')).lower() @@ -1667,12 +2331,18 @@ def _apply_detection_rules( docs_probe_up = any(probe_statuses.get(path) in [200, 301, 302, 401, 403] for path in ['/docs', '/redoc']) # WordPress + wordpress_root_evidence = False if 'wordpress' in generator_lower: self._add_signal('WordPress', self.CMS_CATEGORY, 'meta', 'generator={0}'.format(generator), 7) + wordpress_root_evidence = True if '/wp-content/' in body_lower: self._add_signal('WordPress', self.CMS_CATEGORY, 'markup', '/wp-content/', 6) + wordpress_root_evidence = True if '/wp-includes/' in body_lower: self._add_signal('WordPress', self.CMS_CATEGORY, 'markup', '/wp-includes/', 5) + wordpress_root_evidence = True + if any(cookie.startswith(('wordpress_', 'wp-settings-')) for cookie in cookies): + wordpress_root_evidence = True wordpress_static_probes = ( ('/wp-content/', 6), @@ -1681,14 +2351,44 @@ def _apply_detection_rules( ('/wp-content/themes/', 4), ) for probe_path, weight in wordpress_static_probes: - if self._is_distinct_probe_up(probe_statuses, probe_path, [200, 301, 302, 401, 403], not_found_status): + if webflow_hosted_context and not wordpress_root_evidence: + continue + if self._is_wordpress_probe_up( + probe_statuses, + probe_path, + not_found_status, + wordpress_root_evidence, + public_statuses=[200], + corroborated_statuses=[301, 302, 401, 403], + ): self._add_signal('WordPress', self.CMS_CATEGORY, 'endpoint', probe_path, weight) - if self._is_distinct_probe_up(probe_statuses, '/wp-json/', [200, 401, 403], not_found_status): + if self._is_wordpress_probe_up( + probe_statuses, + '/wp-json/', + not_found_status, + wordpress_root_evidence, + public_statuses=[200], + corroborated_statuses=[401, 403], + ): self._add_signal('WordPress', self.CMS_CATEGORY, 'endpoint', '/wp-json/', 5) - if self._is_distinct_probe_up(probe_statuses, '/wp-login.php', [200, 301, 302, 401, 403], not_found_status): + if self._is_wordpress_probe_up( + probe_statuses, + '/wp-login.php', + not_found_status, + wordpress_root_evidence, + public_statuses=[200], + corroborated_statuses=[301, 302, 401, 403], + ): self._add_signal('WordPress', self.CMS_CATEGORY, 'endpoint', '/wp-login.php', 2) - if self._is_distinct_probe_up(probe_statuses, '/xmlrpc.php', [200, 301, 302, 401, 403, 405], not_found_status): + if self._is_wordpress_probe_up( + probe_statuses, + '/xmlrpc.php', + not_found_status, + wordpress_root_evidence, + public_statuses=[200], + corroborated_statuses=[301, 302, 401, 403, 405], + ): self._add_signal('WordPress', self.CMS_CATEGORY, 'endpoint', '/xmlrpc.php', 2) if any(cookie.startswith(('wordpress_', 'wp-settings-')) for cookie in cookies): self._add_signal('WordPress', self.CMS_CATEGORY, 'cookie', 'wordpress_*', 5) @@ -1776,6 +2476,14 @@ def _apply_detection_rules( self._add_signal('Mobirise', self.SITE_BUILDER_CATEGORY, 'markup', 'mbr-*', 5) # Webflow + if host_lower == 'webflow.io' or host_lower.endswith('.webflow.io'): + self._add_signal('Webflow', self.SITE_BUILDER_CATEGORY, 'host', 'host=*.webflow.io', 9) + if x_wf_region: + self._add_signal('Webflow', self.SITE_BUILDER_CATEGORY, 'header', 'x-wf-region', 8) + if 'webflow.io' in surrogate_key: + self._add_signal('Webflow', self.SITE_BUILDER_CATEGORY, 'header', 'surrogate-key=webflow.io', 7) + if 'webflow.com' in content_security_policy or 'webflow.io' in content_security_policy: + self._add_signal('Webflow', self.SITE_BUILDER_CATEGORY, 'header', 'csp frame-ancestors webflow', 6) if 'webflow' in generator_lower: self._add_signal('Webflow', self.SITE_BUILDER_CATEGORY, 'meta', 'generator={0}'.format(generator), 7) if 'webflow.css' in body_lower or 'w-webflow-' in body_lower: @@ -2071,6 +2779,45 @@ def _apply_detection_rules( if probe_statuses.get('/admin') in [200, 301, 302, 401, 403] and '/bl-themes/' in body_lower: self._add_signal('Bludit', self.CMS_CATEGORY, 'endpoint', '/admin + /bl-themes/', 3) + # MogutaCMS + self._apply_mogutacms_rules( + body_lower=body_lower, + generator=generator, + ) + + # Evolution CMS + self._apply_evolution_cms_rules( + body_lower=body_lower, + generator=generator, + probe_statuses=probe_statuses, + not_found_status=not_found_status, + ) + + # DataLife Engine + self._apply_datalife_engine_rules( + body_lower=body_lower, + generator=generator, + ) + + # Melbis Shop Platform + self._apply_melbis_shop_rules( + body_lower=body_lower, + cookies=cookies, + generator=generator, + ) + + # Camaleon CMS + self._apply_camaleon_cms_rules( + body_lower=body_lower, + cookies=cookies, + generator=generator, + ) + + # CMS.S3 / Megagroup + self._apply_cms_s3_rules( + body_lower=body_lower, + ) + # MODX modx_hint = ( 'modx' in generator_lower @@ -2272,11 +3019,33 @@ def _apply_detection_rules( self._add_signal('Flask', self.FRAMEWORK_CATEGORY, 'header', 'server={0}'.format(headers.get('server')), 7) # Ruby on Rails - if '_rails_session' in cookies: + rails_cookie_hint = '_rails_session' in cookies + rails_exact_csrf = self._has_rails_authenticity_token_meta(body_lower) + rails_csrf_pair = 'csrf-param' in body_lower and 'csrf-token' in body_lower + rails_ujs_hint = self._has_rails_ujs_marker(body_lower) + rails_asset_hint = self._has_rails_asset_marker(body_lower) + rails_error_hint = ( + self._has_rails_error_marker(body_lower) + or self._has_rails_error_marker(not_found_body_lower) + ) + + if rails_cookie_hint: self._add_signal('Ruby on Rails', self.FRAMEWORK_CATEGORY, 'cookie', '_rails_session', 8) - if 'csrf-param' in body_lower and 'csrf-token' in body_lower: + + if rails_exact_csrf: + self._add_signal('Ruby on Rails', self.FRAMEWORK_CATEGORY, 'markup', 'csrf-param=authenticity_token|csrf-token', 8) + elif rails_csrf_pair: self._add_signal('Ruby on Rails', self.FRAMEWORK_CATEGORY, 'markup', 'csrf-param|csrf-token', 5) + if rails_ujs_hint and (rails_cookie_hint or rails_exact_csrf or rails_csrf_pair): + self._add_signal('Ruby on Rails', self.FRAMEWORK_CATEGORY, 'script', 'rails-ujs|turbo-rails', 7) + + if rails_asset_hint and (rails_cookie_hint or rails_exact_csrf or rails_csrf_pair or rails_ujs_hint): + self._add_signal('Ruby on Rails', self.FRAMEWORK_CATEGORY, 'asset', 'rails application asset', 5) + + if rails_error_hint: + self._add_signal('Ruby on Rails', self.FRAMEWORK_CATEGORY, 'exception', 'Rails exception marker', 8) + # Express / NestJS / Fastify / FastAPI / Koa / Hapi if 'express' in x_powered_by or 'express' in not_found_powered_by: self._add_signal('Express', self.FRAMEWORK_CATEGORY, 'header', 'x-powered-by={0}'.format(headers.get('x-powered-by') or not_found_headers.get('x-powered-by')), 8) diff --git a/src/lib/browser/shadow.py b/src/lib/browser/shadow.py index 2976f5ca..e65059dc 100644 --- a/src/lib/browser/shadow.py +++ b/src/lib/browser/shadow.py @@ -24,6 +24,12 @@ class ShadowProbe(object): MAX_SIZE_DELTA_RATIO = 0.25 SHADOW_STATUSES = {200} QUEUE_TIMEOUT_SEC = 0.25 + MIN_CONTROL_CANDIDATES = 2 + MIN_FALLBACK_CONTROL_SIMILARITY = 0.97 + CONTROL_SUFFIX = '.__healthcheck__' + CONTROL_STATE_PENDING = 'pending' + CONTROL_STATE_ALLOWED = 'allowed' + CONTROL_STATE_SUPPRESSED = 'suppressed' SOURCE_EXTENSIONS = { 'asp', 'aspx', 'bash', 'c', 'cfg', 'cgi', 'conf', 'config', 'cpp', 'cs', @@ -75,6 +81,7 @@ def __init__(self, request_callback, match_callback, progress_callback=None, del self.__suffixes = self.load_suffixes() self.__queue = Queue() self.__seen = set() + self.__base_control_states = {} self.__lock = threading.RLock() self.__submitted = 0 self.__completed = 0 @@ -415,6 +422,63 @@ def size_delta_ratio(cls, base_signature, candidate_signature): return abs(base_size - candidate_size) / float(max(base_size, candidate_size)) + @classmethod + def build_control_candidate(cls, url): + """ + Build one deterministic negative-control URL for fallback detection. + + The control keeps the same query string as the original URL so route + fallbacks that depend on query context are still detected, but appends a + suffix that should not exist as a real backup artifact. + + :param str url: base URL + :return: control URL, or None when the base URL cannot be parsed + :rtype: str|None + """ + + try: + parsed = urlsplit(str(url)) + except ValueError: + return None + + path = parsed.path or '' + if not path or path.endswith('/'): + return None + + return urlunsplit((parsed.scheme, parsed.netloc, '{0}{1}'.format(path, cls.CONTROL_SUFFIX), parsed.query, '')) + + @classmethod + def is_fallback_like_control(cls, base_signature, response): + """ + Return True when a negative-control URL behaves like the base response. + + Unlike real shadow matching, byte-identical responses are suspicious + here: a definitely-nonexistent suffix that returns the same page is a + soft-200/fallback signal, not a backup copy. + + :param dict base_signature: base response signature + :param object response: control response + :return: whether shadow candidates for this base URL should be suppressed + :rtype: bool + """ + + control_signature = cls.response_signature(response) + if not isinstance(base_signature, dict) or control_signature is None: + return False + + base_content_type = str(base_signature.get('content_type', '')) + control_content_type = str(control_signature.get('content_type', '')) + if base_content_type and control_content_type and base_content_type != control_content_type: + return False + + if cls.size_delta_ratio(base_signature, control_signature) > cls.MAX_SIZE_DELTA_RATIO: + return False + + if control_signature.get('hash') == base_signature.get('hash'): + return True + + return cls.similarity_ratio(base_signature, control_signature) >= cls.MIN_FALLBACK_CONTROL_SIMILARITY + @classmethod def is_match(cls, base_signature, response): """ @@ -502,8 +566,15 @@ def enqueue(self, base_url, base_response, bucket): if base_signature is None: return 0 + candidates = self.build_candidates(base_url, self.__suffixes) + with self.__lock: + if self.__submitted >= self.MAX_TOTAL_PROBES: + return 0 + + self.__enqueue_control_candidate(base_url, base_signature, candidates) + queued = 0 - for candidate_url, suffix in self.build_candidates(base_url, self.__suffixes): + for candidate_url, suffix in candidates: with self.__lock: if self.__submitted >= self.MAX_TOTAL_PROBES: break @@ -513,11 +584,39 @@ def enqueue(self, base_url, base_response, bucket): self.__submitted += 1 current = self.__submitted - self.__queue.put((base_url, base_signature, candidate_url, suffix, current)) + self.__queue.put(('candidate', base_url, base_signature, candidate_url, suffix, current)) queued += 1 return queued + def __enqueue_control_candidate(self, base_url, base_signature, candidates): + """ + Queue one internal negative-control probe before candidate probes. + + Controls are intentionally not counted in submitted/completed candidate + counters. They only decide whether a base URL is a soft-200/fallback + source that would otherwise create a burst of false shadow findings. + + :param str base_url: original base URL + :param dict base_signature: normalized base response signature + :param list[tuple[str, str]] candidates: generated shadow candidates + :return: None + """ + + if len(candidates) < self.MIN_CONTROL_CANDIDATES: + return + + control_url = self.build_control_candidate(base_url) + if not control_url: + return + + with self.__lock: + if base_url in self.__base_control_states: + return + self.__base_control_states[base_url] = self.CONTROL_STATE_PENDING + + self.__queue.put(('control', base_url, base_signature, control_url, self.CONTROL_SUFFIX, 0)) + def drain(self): """ Wait until all submitted shadow probes have completed. @@ -535,8 +634,9 @@ def __run(self): try: self.__process_task(task) finally: - with self.__lock: - self.__completed += 1 + if self.__is_control_task(task) is not True: + with self.__lock: + self.__completed += 1 self.__queue.task_done() def __process_task(self, task): @@ -547,7 +647,14 @@ def __process_task(self, task): :return: None """ - base_url, base_signature, candidate_url, suffix, current = task + task_type, base_url, base_signature, candidate_url, suffix, current = self.__normalize_task(task) + + if task_type == 'control': + self.__process_control_task(base_url, base_signature, candidate_url) + return + + if self.__is_base_suppressed(base_url) is True: + return if self.__delay > 0: time.sleep(self.__delay) @@ -570,3 +677,83 @@ def __process_task(self, task): if callable(self.__match_callback): self.__match_callback(candidate_url, candidate_response, metadata) + + @classmethod + def __normalize_task(cls, task): + """ + Normalize legacy and current shadow task tuples. + + :param tuple task: queued shadow task + :return: task type, base URL, base signature, candidate URL, suffix and counter + :rtype: tuple[str, str, dict, str, str, int] + """ + + if len(task) == 6: + return task + + base_url, base_signature, candidate_url, suffix, current = task + return 'candidate', base_url, base_signature, candidate_url, suffix, current + + @classmethod + def __is_control_task(cls, task): + """ + Return whether a queued task is an internal fallback-control task. + + :param tuple task: queued shadow task + :return: whether task is a control probe + :rtype: bool + """ + + try: + return len(task) == 6 and task[0] == 'control' + except TypeError: + return False + + def __process_control_task(self, base_url, base_signature, control_url): + """ + Process one internal negative-control probe. + + :param str base_url: original base URL + :param dict base_signature: base response signature + :param str control_url: generated control URL + :return: None + """ + + if self.__delay > 0: + time.sleep(self.__delay) + + try: + control_response = self.__request_callback(control_url) + except Exception: + self.__set_base_control_state(base_url, self.CONTROL_STATE_ALLOWED) + return + + if self.is_fallback_like_control(base_signature, control_response) is True: + self.__set_base_control_state(base_url, self.CONTROL_STATE_SUPPRESSED) + return + + self.__set_base_control_state(base_url, self.CONTROL_STATE_ALLOWED) + + def __set_base_control_state(self, base_url, state): + """ + Store fallback-control state for one base URL. + + :param str base_url: original base URL + :param str state: control state + :return: None + """ + + with self.__lock: + self.__base_control_states[str(base_url)] = state + + def __is_base_suppressed(self, base_url): + """ + Return whether shadow candidates for a base URL must be skipped. + + :param str base_url: original base URL + :return: whether candidates are suppressed as fallback noise + :rtype: bool + """ + + with self.__lock: + return self.__base_control_states.get(str(base_url)) == self.CONTROL_STATE_SUPPRESSED diff --git a/src/lib/browser/threadpool.py b/src/lib/browser/threadpool.py index a0d836a6..78e2d1f6 100644 --- a/src/lib/browser/threadpool.py +++ b/src/lib/browser/threadpool.py @@ -16,6 +16,7 @@ Development: Stanislav WEB """ +import threading import time from queue import Queue @@ -30,6 +31,8 @@ class ThreadPool(object): JOIN_POLL_INTERVAL_SEC = 1.0 JOIN_STALL_WARNING_SEC = 60.0 + PAUSE_PROMPT_DRAIN_TIMEOUT_SEC = 0.5 + PAUSE_PROMPT_DRAIN_POLL_SEC = 0.05 def __init__(self, num_threads, total_items, timeout, stall_warning_interval=None): """ @@ -44,6 +47,8 @@ def __init__(self, num_threads, total_items, timeout, stall_warning_interval=Non self.__workers = [] self.__submitted = 0 self.__worker_error = None + self.__pause_requested = False + self.__pause_lock = threading.RLock() self.total_items_size = total_items self.is_started = True self.__stall_warning_interval = self.__normalize_stall_warning_interval(stall_warning_interval) @@ -145,12 +150,79 @@ def add(self, func, *args, **kargs): :return: None """ - try: - if True is self.is_started: - if self.__submitted < self.total_items_size: - self.__queue.put((func, args, kargs)) - self.__submitted += 1 - except (SystemExit, KeyboardInterrupt): + if True is not self.is_started: + return + + if self.__submitted >= self.total_items_size: + return + + self.__pause_if_requested() + + if True is not self.is_started: + return + + self.__enqueue_with_pause_resume(func, args, kargs) + + def __enqueue_with_pause_resume(self, func, args, kargs): + """ + Enqueue a task and preserve it across the runtime pause prompt. + + If Ctrl+C is pressed while the main thread is submitting a task, the + pause prompt must not silently drop the current queue item when the user + continues the scan. + + :param func func: callback function + :param tuple args: callback positional arguments + :param dict kargs: callback keyword arguments + :raise KeyboardInterrupt: when the user aborts from the pause prompt + :return: None + """ + + while True: + try: + self.__queue.put((func, args, kargs)) + self.__submitted += 1 + return + except (SystemExit, KeyboardInterrupt): + self.pause() + + + def request_pause(self): + """ + Request the regular runtime pause prompt from another thread. + + Worker threads must not show the interactive prompt directly because an + abort answer raises ``KeyboardInterrupt`` outside the main scan thread. + This method only marks the pool as pause-requested and pauses workers + before their next queued task. ``add()`` or ``join()`` will show the + existing prompt on the controlling thread. + + :return: True when a new pause request was registered + :rtype: bool + """ + + with self.__pause_lock: + if self.__pause_requested is True or self.is_started is not True: + return False + + self.__pause_requested = True + + for worker in self.__workers: + worker.pause() + + return True + + def __pause_if_requested(self): + """Open the regular pause menu when a worker requested it. + + :raise KeyboardInterrupt: when the user aborts from the pause prompt + :return: None + """ + + with self.__pause_lock: + requested = self.__pause_requested is True + + if requested is True: self.pause() def join(self): @@ -171,12 +243,15 @@ def join(self): with self.__queue.all_tasks_done: while int(getattr(self.__queue, 'unfinished_tasks', 0) or 0) > 0: + self.__pause_if_requested() + try: self.__queue.all_tasks_done.wait(timeout=self.JOIN_POLL_INTERVAL_SEC) except (SystemExit, KeyboardInterrupt): self.pause() continue + self.__pause_if_requested() self.__raise_worker_error_if_any() completed = self.completed_size @@ -274,6 +349,26 @@ def __active_tasks_signature(self): return tuple(signature) + def __wait_for_pause_prompt_drain(self): + """ + Give in-flight worker output a short chance to drain before prompting. + + Runtime pause does not kill active requests. A worker can therefore + finish and print its scan result right after Ctrl+C. Waiting briefly + before showing the prompt keeps the prompt visible instead of letting it + be interleaved with the last in-flight result. + + The wait is intentionally bounded so a slow or retrying request cannot + hide the pause prompt indefinitely. + + :return: None + """ + + deadline = time.monotonic() + float(self.PAUSE_PROMPT_DRAIN_TIMEOUT_SEC) + + while len(self.active_tasks) > 0 and time.monotonic() < deadline: + time.sleep(float(self.PAUSE_PROMPT_DRAIN_POLL_SEC)) + def __format_active_tasks(self, now): """ Format active worker tasks for join watchdog diagnostics. @@ -348,15 +443,20 @@ def pause(self): :return: None """ + with self.__pause_lock: + self.__pause_requested = False + self.is_started = False tpl.info(key='stop_threads', threads=len(self.__workers)) for worker in self.__workers: worker.pause() + self.__wait_for_pause_prompt_drain() + try: while True: - option = self.normalize_runtime_pause_answer(tpl.prompt(key='option_prompt')) + option = self.normalize_runtime_pause_answer(tpl.prompt(key='option_prompt', newline=True)) if option == 'E': raise KeyboardInterrupt @@ -364,6 +464,8 @@ def pause(self): self.resume() return + tpl.warning(key='unknown_pause_command') + except (SystemExit, KeyboardInterrupt): raise KeyboardInterrupt diff --git a/src/lib/reader/reader.py b/src/lib/reader/reader.py index 797643ff..b11bc42c 100644 --- a/src/lib/reader/reader.py +++ b/src/lib/reader/reader.py @@ -318,6 +318,25 @@ def _subdomains__line(cls, line, params): return params.get('scheme') + line + '.' + host + port + @staticmethod + def _normalize_directory_prefix(prefix): + """ + Normalize directory scan prefix as a path segment. + + :param str|None prefix: raw prefix from CLI/config/session + :return: normalized prefix with trailing slash, or an empty string + """ + + if prefix is None: + return '' + + prefix = str(prefix).strip().strip('/') + + if not prefix: + return '' + + return '{0}/'.format(prefix) + def _directories__line(self, line, params): """ Read lines from directories file @@ -332,6 +351,8 @@ def _directories__line(self, line, params): if prefix is None: prefix = self.__browser_config.get('prefix', '') + prefix = self._normalize_directory_prefix(prefix) + if prefix: line = prefix + line diff --git a/src/lib/reporter/plugins/txt.py b/src/lib/reporter/plugins/txt.py index 9baffb70..075c203e 100644 --- a/src/lib/reporter/plugins/txt.py +++ b/src/lib/reporter/plugins/txt.py @@ -92,6 +92,27 @@ def __format_signal_list(cls, value): return str(value) + def format_transport_failed_items(self): + """Format transport-exhausted request paths for a dedicated txt report. + + These entries are not findings. They identify dictionary items that + were consumed but did not receive any HTTP response after the + configured timeout/retry budget. + + :return: formatted report lines + :rtype: list[str] + """ + + items = self._data.get('transport_failed') + if not isinstance(items, list): + return [] + + return [ + self.format_report_item(item) + for item in items + if item is not None + ] + def format_fingerprint_summary(self): fingerprint = self._data.get('fingerprint') if not isinstance(fingerprint, dict) or len(fingerprint) == 0: @@ -168,6 +189,9 @@ def process(self): if status not in ['failed']: data = [self.format_report_item(item) for item in self.get_report_items(status)] self.record(self.__target_dir, status, data, '\n') + transport_failed_data = self.format_transport_failed_items() + if len(transport_failed_data) > 0: + self.record(self.__target_dir, 'transport_failed', transport_failed_data, '\n') fingerprint_data = self.format_fingerprint_summary() if len(fingerprint_data) > 0: self.record(self.__target_dir, 'fingerprint', fingerprint_data, '\n') diff --git a/src/lib/tpl/config.py b/src/lib/tpl/config.py index 8b9f3575..64105ab4 100644 --- a/src/lib/tpl/config.py +++ b/src/lib/tpl/config.py @@ -45,9 +45,10 @@ class Config(object): 'random_browser': 'Fetching random user-agent per request...', 'total_time_lvl3': 'Total time running: {time}', 'thread_limit': 'Threads has been reduced to {max} (max) instead of {threads}', - 'stop_threads': 'Stopping threads ({threads})...', - 'option_prompt': 'Press "[C]ontinue" to resume or "[E]xit" to abort session: ', + 'stop_threads': 'Pausing workers ({threads}). Active requests may finish first...', + 'option_prompt': 'Scan paused. Press Enter/[C] to continue or [E]/[Q] to abort scan: ', 'resume_threads': 'Resuming scan...', + 'unknown_pause_command': 'Unknown command. Use Enter/C to continue or E/Q to abort scan.', 'get_item': '{percent} [{current}/{total}] - {code} - {size} - {item}', 'scan_progress': '{percent} [{current}/{total}]', 'fingerprint_progress': 'Fingerprint {bar} {stage}', @@ -71,6 +72,7 @@ class Config(object): 'proxy_max_retry_error': 'Skipped. Proxy {proxy} Max retries exceeded: {url}', 'host_changed_error': 'Block external redirect -> {details}', 'read_timeout_error': 'Read timeout error! {url}. Increase using --timeout option', + 'decode_error': 'Response decode error! {url}. Skipping corrupted encoded response', 'connection_timeout_error': 'Connection timeout error! {url}. Increase using --timeout option', 'certificate': 'Cert required {url}', 'success': 'OK {url}', diff --git a/src/lib/tpl/tpl.py b/src/lib/tpl/tpl.py index 938b7a59..b9c222d1 100644 --- a/src/lib/tpl/tpl.py +++ b/src/lib/tpl/tpl.py @@ -71,12 +71,13 @@ def line_log(msg='', key='', status='info', write=True, **args): raise TplError(error) @staticmethod - def prompt(key='', msg=None, status='info', **kwargs): + def prompt(key='', msg=None, status='info', newline=False, **kwargs): """ Prompt message :param str key: tpl message key :param str msg: target message :param str status: message status + :param bool newline: print prompt text as a full line before reading input :param dict kwargs: additional key arguments :raise TplError :return:str @@ -88,6 +89,11 @@ def prompt(key='', msg=None, status='info', **kwargs): msg = Tpl.line_log(key=key, status=status, write=False) else: msg = Tpl.line_log(msg, status=status, write=False, **kwargs) + if newline is True: + Tpl.__finish_dynamic_line() + sys.writeln(msg) + return input('') + result = input(msg) return result diff --git a/tests/test_core_http_plugins_response_malware.py b/tests/test_core_http_plugins_response_malware.py index 5847d75b..76f43010 100644 --- a/tests/test_core_http_plugins_response_malware.py +++ b/tests/test_core_http_plugins_response_malware.py @@ -27,6 +27,36 @@ def make_response(self, status=200, body=b'', headers=None): return HTTPResponse(status=status, body=body, headers=headers or {'Content-Type': 'text/html'}) + @staticmethod + def make_bitrix_login_body(extra=''): + """ + Build a compact Bitrix admin login page fixture. + + :param str extra: optional extra HTML inserted into body + :return: HTML fixture + :rtype: str + """ + + return ''' + + + + + + + + +
+ +
+ + {0} + + + '''.format(extra) + def assert_malware_detection(self, body, subtype, family, confidence=70, headers=None): """ Assert that a response body is classified as malware. @@ -72,6 +102,189 @@ def test_detects_known_webshell_markers_as_malware(self): self.assertEqual(detection['type'], 'malware') self.assertEqual(detection['subtype'], 'webshell') + def test_ignores_security_documentation_known_webshell_vocabulary(self): + """Security-plugin documentation should not trigger on name-only webshell vocabulary.""" + + body = ''' + === Security Scanner - Firewall and Malware Scan === + Contributors: security-team + Requires at least: 5.0 + Stable tag: 1.0.0 + + == Description == + This WordPress security plugin includes a web application firewall, + malware scanner, firewall rules, malware signatures, and a threat defense feed. + The scanner checks core files, themes and plugins for known backdoors + including WSO Shell, C99, R57, China Chopper, and WebShell variants. + + == Frequently Asked Questions == + The documentation explains how security scanner results are displayed. + ''' + response = self.make_response( + body=body.encode('utf-8'), + headers={'Content-Type': 'text/plain'}, + ) + + self.assertIsNone(MalwareResponsePlugin(None).process(response)) + self.assertFalse(hasattr(response, 'opendoor_malware_detection')) + + def test_ignores_known_webshell_name_when_only_reflected_in_url_attributes(self): + """Fallback pages should not trigger malware from path-only URL echoes.""" + + body = ''' + + + + Product catalogue + + + + + + Continue browsing +
+ +
+

Ordinary fallback template for a missing catalogue route.

+ + + ''' + response = self.make_response(body=body.encode('utf-8')) + + self.assertIsNone(MalwareResponsePlugin(None).process(response)) + self.assertFalse(hasattr(response, 'opendoor_malware_detection')) + + def test_ignores_repeated_webshell_names_reflected_in_url_query_attributes(self): + """Fallback language links should not trigger from repeated URL query echoes.""" + + body = ''' + + + + Product catalogue + + + + Türkçe + English + Русский + العربية +

Ordinary fallback template with language switcher links.

+ + + ''' + response = self.make_response(body=body.encode('utf-8')) + + self.assertIsNone(MalwareResponsePlugin(None).process(response)) + self.assertFalse(hasattr(response, 'opendoor_malware_detection')) + + def test_still_detects_known_webshell_name_when_url_echo_has_shell_ui_context(self): + """URL echoes must not hide real shell UI indicators nearby.""" + + body = ''' + + + + + WSO Shell + + + Current directory: /var/www/html +
+ + +
+ chmod safe_mode disable_functions + + + ''' + + detection = self.assert_malware_detection( + body, + 'webshell', + 'known-webshell-marker', + 98, + ) + + self.assertIn('known-webshell-name', detection['signals']) + + def test_still_detects_executable_payload_when_known_name_is_only_url_echo(self): + """Path-only suppression must not hide strong executable payload signals.""" + + body = ''' + + + + + + + + + + ''' + + detection = self.assert_malware_detection( + body, + 'webshell', + 'known-webshell-marker', + 98, + ) + + self.assertIn('request-driven-command-exec', detection['signals']) + + def test_still_detects_known_webshell_name_with_shell_ui_context(self): + """Documentation markers must not hide a real webshell-like UI context.""" + + body = ''' + Contributors: security-team + Stable tag: 1.0.0 + == Description == + Security scanner documentation header. + + + WSO Shell + + FilesMan + Current directory: /var/www/html +
+ + +
+ chmod safe_mode disable_functions + + + ''' + + detection = self.assert_malware_detection( + body, + 'webshell', + 'known-webshell-marker', + 98, + ) + + self.assertIn('known-webshell-name', detection['signals']) + + def test_still_detects_executable_payload_inside_security_documentation(self): + """Documentation context should suppress only weak name-only signals.""" + + body = ''' + Contributors: security-team + Stable tag: 1.0.0 + == Description == + Security scanner documentation with malware signatures and firewall rules. + Example leaked payload: + ''' + + detection = self.assert_malware_detection( + body, + 'webshell', + 'php-command-execution', + 96, + headers={'Content-Type': 'text/plain'}, + ) + + self.assertIn('request-driven-command-exec', detection['signals']) + def test_detects_request_driven_command_execution(self): """PHP request-driven command execution should be treated as malware/webshell evidence.""" @@ -97,6 +310,84 @@ def test_detects_encoded_loader_hidden_iframe_and_miner_markers(self): with self.subTest(family=family): self.assert_malware_detection(body, subtype, family, confidence) + def test_ignores_legacy_google_analytics_document_write_unescape_loader(self): + """Classic GA document.write(unescape(...ga.js...)) should not be malware.""" + + body = ''' + + + ''' + response = self.make_response(body=body.encode('utf-8')) + + self.assertIsNone(MalwareResponsePlugin(None).process(response)) + self.assertFalse(hasattr(response, 'opendoor_malware_detection')) + + def test_ignores_split_host_legacy_google_analytics_document_write_loader(self): + """Classic GA split gaJsHost loader should not trigger drive-by detection.""" + + body = ''' + + + ''' + response = self.make_response(body=body.encode('utf-8')) + + self.assertIsNone(MalwareResponsePlugin(None).process(response)) + self.assertFalse(hasattr(response, 'opendoor_malware_detection')) + + def test_still_detects_unknown_document_write_unescape_script_loader(self): + """Unknown document.write(unescape(...script...)) loaders remain reportable.""" + + body = ''' + + ''' + + detection = self.assert_malware_detection(body, 'malware', 'drive-by-script', 72) + self.assertIn('suspicious-document-write-script', detection['signals']) + + def test_still_detects_document_write_atob_loader(self): + """The GA allowlist must not suppress atob based document.write loaders.""" + + body = '' + + detection = self.assert_malware_detection(body, 'malware', 'drive-by-script', 72) + self.assertIn('suspicious-document-write-script', detection['signals']) + + def test_still_detects_document_write_string_from_char_code_loader(self): + """The GA allowlist must not suppress String.fromCharCode loaders.""" + + body = '' + + detection = self.assert_malware_detection(body, 'malware', 'drive-by-script', 72) + self.assertIn('suspicious-document-write-script', detection['signals']) + + def test_google_analytics_allowlist_does_not_affect_php_payload_signals(self): + """The GA allowlist must not alter PHP eval/system detections.""" + + samples = ( + ("", 'php-command-execution', 'request-driven-command-exec'), + ("", 'php-code-execution', 'request-driven-eval-or-assert'), + ) + + for body, family, signal in samples: + with self.subTest(signal=signal): + detection = self.assert_malware_detection(body, 'webshell', family, 96, headers={'Content-Type': 'text/plain'}) + self.assertIn(signal, detection['signals']) + def test_detects_vendor_json_content_types(self): """Vendor JSON responses should be inspected when they carry malware indicators.""" @@ -121,6 +412,46 @@ def test_ignores_google_tag_manager_noscript_hidden_iframe(self): self.assertIsNone(MalwareResponsePlugin(None).process(response)) self.assertFalse(hasattr(response, 'opendoor_malware_detection')) + def test_ignores_bitrix_admin_login_auth_frame_hidden_iframe(self): + """Standard Bitrix auth_frame iframe should not be treated as malware.""" + + body = self.make_bitrix_login_body() + response = self.make_response(body=body.encode('utf-8')) + + self.assertIsNone(MalwareResponsePlugin(None).process(response)) + self.assertFalse(hasattr(response, 'opendoor_malware_detection')) + + def test_still_detects_auth_frame_hidden_iframe_without_bitrix_context(self): + """auth_frame by itself must not disable hidden iframe detection.""" + + self.assert_malware_detection( + '', + 'malware', + 'hidden-iframe-injection', + 82, + ) + + def test_still_detects_external_hidden_iframe_on_bitrix_login_page(self): + """Bitrix allowlist must not hide injected external hidden iframes.""" + + body = self.make_bitrix_login_body( + '' + ) + + detection = self.assert_malware_detection(body, 'malware', 'hidden-iframe-injection', 82) + self.assertEqual(detection['count'], 1) + self.assertIn('https://bad.example', detection['matches'][0]['evidence']) + + def test_still_detects_non_empty_bitrix_auth_frame_src(self): + """Bitrix auth_frame allowlist must require an empty iframe source.""" + + body = self.make_bitrix_login_body().replace( + 'name="auth_frame" src=""', + 'name="auth_frame" src="https://bad.example/payload.html"', + ) + + self.assert_malware_detection(body, 'malware', 'hidden-iframe-injection', 82) + def test_still_detects_non_gtm_hidden_iframe(self): """Allowlisting GTM must not disable hidden iframe detection for unknown hosts.""" @@ -244,6 +575,49 @@ def test_get_header_returns_case_insensitive_match(self): self.assertEqual(MalwareResponsePlugin._get_header(response, 'Content-Type'), 'Application/XML; charset=utf-8') + def test_extract_js_statement_without_semicolon_is_bounded(self): + """JavaScript statement extraction should stay bounded when no semicolon exists.""" + + source = '" + ) + response_object = SimpleNamespace( + status=200, + headers={'Content-Type': 'text/html'}, + data=body.encode('utf-8'), + ) + + client = MagicMock() + client.request.return_value = response_object + pool = SimpleNamespace(items_size=1, total_items_size=3) + reader = MagicMock() + reader.get_ignored_list.return_value = [] + response_handler = MagicMock() + response_handler.handle.return_value = ( + 'success', + 'http://www.madburg.ru/adminer-4.2.3-sk.php', + '273B', + '200', + ) + + setattr(br, '_Browser__client', client) + setattr(br, '_Browser__pool', pool) + setattr(br, '_Browser__reader', reader) + setattr(br, '_Browser__response', response_handler) + + br._Browser__http_request('http://www.madburg.ru/adminer-4.2.3-sk.php') + + result = getattr(br, '_Browser__result') + self.assertEqual(result['total']['success'], 0) + self.assertEqual(result['total']['calibrated'], 1) + self.assertEqual( + result['items']['calibrated'], + ['http://www.madburg.ru/adminer-4.2.3-sk.php'], + ) + self.assertEqual( + result['report_items']['calibrated'][0]['calibration_reason'], + 'js-cookie-reload-challenge', + ) + response_handler.debug_response_data.assert_not_called() + + def test_js_cookie_reload_challenge_helper_rejects_useful_pages(self): + """JS cookie challenge guard should not hide useful HTML pages.""" + + useful_response = SimpleNamespace( + status=200, + headers={'Content-Type': 'text/html'}, + data=( + "" + "
" + ).encode('utf-8'), + ) + + actual = Browser._Browser__match_js_cookie_reload_challenge( + useful_response, + ('success', 'http://example.com/login', '130B', '200'), + ) + + self.assertIsNone(actual) + + def test_js_cookie_reload_challenge_helper_handles_multiline_script_end_tag(self): + """JS cookie challenge guard should ignore script bodies without regex HTML filtering.""" + + response = SimpleNamespace( + status=200, + headers={'Content-Type': 'text/html'}, + data=( + "' + '' + 'News site', + {}, + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse(404, 'Not Found', {}), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'cms') + self.assertEqual(result['name'], 'DataLife Engine') + self.assertEqual(result['runtime']['name'], 'PHP') + self.assertIn('engine/classes/js/dle_js.js', [signal['value'] for signal in result['signals']]) + + + def test_detects_datalife_engine_from_search_runtime_globals(self): + """Fingerprint should detect DLE from inline search globals used on real homepages.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + 'News', + {}, + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse(404, 'Not Found', {}), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'cms') + self.assertEqual(result['name'], 'DataLife Engine') + self.assertEqual(result['runtime']['name'], 'PHP') + self.assertTrue( + any('dle_search_delay' in signal['value'] for signal in result['signals']) + ) + + def test_detects_datalife_engine_from_explicit_generator(self): + """Fingerprint should keep explicit DataLife Engine generator support.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + '', + {}, + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse(404, 'Not Found', {}), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['name'], 'DataLife Engine') + self.assertGreaterEqual(result['confidence'], 70) + + def test_does_not_promote_datalife_engine_from_weak_dle_text_only(self): + """Fingerprint should not classify generic DLE text or one isolated global as DLE.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + 'DLE can mean distance learning environment. ' + '', + {}, + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse(404, 'Not Found', {}), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertNotEqual(result['name'], 'DataLife Engine') + self.assertEqual(result['name'], 'Unknown custom stack') + def test_detects_nextjs(self): """Fingerprint should detect Next.js from __NEXT_DATA__ and _next assets.""" @@ -274,6 +377,401 @@ def test_detects_webflow(self): self.assertEqual(result['category'], 'sitebuilder') self.assertEqual(result['name'], 'Webflow') + def test_detects_webflow_hosted_site_and_ignores_wordpress_endpoint_artifacts(self): + """Fingerprint should prefer Webflow hosting/header signals over endpoint-only WordPress artifacts.""" + + config = FakeConfig(host='carrotdev.webflow.io', scheme='https://', port=443) + base = 'https://carrotdev.webflow.io/' + responses = { + ('GET', base): FakeResponse( + 200, + 'CarrotDevsWeb design', + { + 'Server': 'cloudflare', + 'X-WF-Region': 'us-east-1', + 'Surrogate-Key': 'carrotdev.webflow.io pageId:abc', + 'Content-Security-Policy': ( + "frame-ancestors 'self' https://*.webflow.com " + 'http://*.webflow.io https://webflow.com' + ), + }, + ), + ('HEAD', 'https://carrotdev.webflow.io/wp-content/'): FakeResponse(403, '', {}), + ('HEAD', 'https://carrotdev.webflow.io/wp-includes/'): FakeResponse(403, '', {}), + ('HEAD', 'https://carrotdev.webflow.io/wp-content/plugins/'): FakeResponse(403, '', {}), + ('HEAD', 'https://carrotdev.webflow.io/wp-content/themes/'): FakeResponse(403, '', {}), + ('GET', 'https://carrotdev.webflow.io{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {'X-WF-Region': 'us-east-1'}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'sitebuilder') + self.assertEqual(result['name'], 'Webflow') + self.assertFalse(any(candidate['name'] == 'WordPress' for candidate in result['candidates'])) + self.assertIn('host=*.webflow.io', [signal['value'] for signal in result['signals']]) + + def test_detects_cms_s3_from_root_builder_runtime_markers(self): + """Fingerprint should detect CMS.S3 without relying on WordPress endpoint probes.""" + + config = FakeConfig(host='evroavtofurgon.ru', scheme='https://', port=443) + base = 'https://evroavtofurgon.ru/' + responses = { + ('GET', base): FakeResponse( + 200, + '' + '' + '' + '' + '
' + '' + 'Изготовление сайтов: megagroup.ru' + '', + {'Server': 'nginx'}, + ), + ('HEAD', 'https://evroavtofurgon.ru/wp-content/'): FakeResponse(403, '', {}), + ('HEAD', 'https://evroavtofurgon.ru/wp-includes/'): FakeResponse(403, '', {}), + ('HEAD', 'https://evroavtofurgon.ru/wp-content/plugins/'): FakeResponse(403, '', {}), + ('HEAD', 'https://evroavtofurgon.ru/wp-content/themes/'): FakeResponse(403, '', {}), + ('GET', 'https://evroavtofurgon.ru{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'cms') + self.assertEqual(result['name'], 'CMS.S3 / Megagroup') + self.assertEqual(result['runtime']['name'], 'PHP') + self.assertFalse(any(candidate['name'] == 'WordPress' for candidate in result['candidates'])) + self.assertIn( + '/shared/s3/', + ''.join(signal['value'] for signal in result['signals']), + ) + + def test_weak_cms_s3_vendor_text_stays_below_detection_threshold(self): + """A single Megagroup footer/link should not classify an unrelated site as CMS.S3.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + 'vendor portfolio link', + {}, + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'custom') + self.assertEqual(result['name'], 'Unknown custom stack') + self.assertEqual(result['candidates'][0]['name'], 'CMS.S3 / Megagroup') + self.assertLess(result['candidates'][0]['score'], 7) + + + def test_detects_melbis_shop_from_powered_footer(self): + """Fingerprint should detect Melbis Shop from an explicit product footer.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + '' + '
© 2026 Melbis. Powered by Melbis Shop v6.5.0.279
' + 'Product' + '', + {}, + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'ecommerce') + self.assertEqual(result['name'], 'Melbis Shop Platform') + self.assertEqual(result['runtime']['name'], 'PHP') + self.assertIn('Powered by Melbis Shop', [signal['value'] for signal in result['signals']]) + + def test_detects_melbis_shop_from_legacy_russian_footer_and_cookie(self): + """Fingerprint should detect legacy Melbis Shop storefront markers.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + '' + 'Каталог' + 'Товар' + '
Магазин создан на базе Melbis Shop v5.4.0. ' + 'Последнее обновление магазина: 25-06-2025 11:00
' + '', + MultiHeaders([('Set-Cookie', 'MS_MSS=abc123; path=/')]), + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'ecommerce') + self.assertEqual(result['name'], 'Melbis Shop Platform') + self.assertIn('MS_MSS', [signal['value'] for signal in result['signals']]) + + def test_detects_melbis_shop_from_default_template_assets(self): + """Fingerprint should detect Melbis Shop from default template assets.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + '' + '' + '' + 'Catalog', + {}, + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'ecommerce') + self.assertEqual(result['name'], 'Melbis Shop Platform') + + def test_does_not_detect_melbis_shop_from_ms_mss_cookie_alone(self): + """MS_MSS is a weak corroborating cookie and should not classify alone.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + '

Generic PHP storefront

', + MultiHeaders([('Set-Cookie', 'MS_MSS=abc123; path=/')]), + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertNotEqual(result['name'], 'Melbis Shop Platform') + self.assertFalse(any(candidate['name'] == 'Melbis Shop Platform' for candidate in result['candidates'])) + + def test_does_not_detect_melbis_shop_from_generic_article_text(self): + """Generic Melbis Shop mentions should not become a storefront fingerprint.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + '
Review of Melbis Shop and other e-commerce platforms.
', + {}, + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'custom') + self.assertEqual(result['name'], 'Unknown custom stack') + self.assertFalse(any(candidate['name'] == 'Melbis Shop Platform' for candidate in result['candidates'])) + + + def test_detects_camaleon_cms_from_official_site_runtime_markers(self): + """Fingerprint should detect Camaleon CMS from brand context plus Rails CSRF.""" + + config = FakeConfig(host='camaleon.website', scheme='https://', port=443) + base = 'https://camaleon.website/' + responses = { + ('GET', base): FakeResponse( + 200, + '' + '' + '' + '' + '

CAMALEON CMS

' + '

Camaleon CMS is an advanced Content Management System.

' + '

Wordpress Alternative. Built with Ruby-on-Rails.

' + 'Plugins Store' + '
Copyright © 2015 - 2018 CamaleonCMS. All rights reserved.
' + '', + {}, + ), + ('GET', 'https://camaleon.website{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'cms') + self.assertEqual(result['name'], 'Camaleon CMS') + self.assertEqual(result['runtime']['name'], 'Ruby') + self.assertIn('Camaleon CMS+Rails CSRF', [signal['value'] for signal in result['signals']]) + + def test_detects_camaleon_cms_from_admin_assets_and_cookies(self): + """Fingerprint should detect Camaleon CMS from strong admin asset markers.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + '' + '' + '' + '', + MultiHeaders([ + ('Set-Cookie', 'auth_token=abc; Path=/'), + ('Set-Cookie', '_cms_session=xyz; Path=/; HttpOnly'), + ]), + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'cms') + self.assertEqual(result['name'], 'Camaleon CMS') + self.assertEqual(result['runtime']['name'], 'Ruby') + self.assertIn('auth_token+_cms_session', [signal['value'] for signal in result['signals']]) + + def test_does_not_detect_camaleon_cms_from_brand_text_without_rails_context(self): + """Generic Camaleon CMS text should not become a fingerprint by itself.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + '
Review of Camaleon CMS for Rails developers.
', + {}, + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'custom') + self.assertEqual(result['name'], 'Unknown custom stack') + self.assertFalse(any(candidate['name'] == 'Camaleon CMS' for candidate in result['candidates'])) + + + def test_does_not_detect_wordpress_from_generic_restricted_static_probes(self): + """Fingerprint should not treat generic 403/405 WordPress path denies as WordPress.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + 'CMS.S3-like site' + '', + {'Server': 'nginx'}, + ), + ('HEAD', 'http://example.com/wp-content/'): FakeResponse(403, '', {}), + ('HEAD', 'http://example.com/wp-includes/'): FakeResponse(403, '', {}), + ('HEAD', 'http://example.com/wp-content/plugins/'): FakeResponse(403, '', {}), + ('HEAD', 'http://example.com/wp-content/themes/'): FakeResponse(403, '', {}), + ('HEAD', 'http://example.com/wp-json/'): FakeResponse(403, '', {}), + ('HEAD', 'http://example.com/wp-login.php'): FakeResponse(403, '', {}), + ('HEAD', 'http://example.com/xmlrpc.php'): FakeResponse(405, '', {}), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'custom') + self.assertEqual(result['name'], 'Unknown custom stack') + self.assertFalse(any(candidate['name'] == 'WordPress' for candidate in result['candidates'])) + + def test_does_not_detect_wordpress_from_404_static_probes(self): + """Fingerprint should not treat 404 WordPress static paths as reachable endpoints.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse(200, 'plain site', {}), + ('HEAD', 'http://example.com/wp-content/'): FakeResponse(404, '', {}), + ('HEAD', 'http://example.com/wp-includes/'): FakeResponse(404, '', {}), + ('HEAD', 'http://example.com/wp-content/plugins/'): FakeResponse(404, '', {}), + ('HEAD', 'http://example.com/wp-content/themes/'): FakeResponse(404, '', {}), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse( + 404, + 'Not Found', + {}, + ), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'custom') + self.assertEqual(result['name'], 'Unknown custom stack') + self.assertFalse(any(candidate['name'] == 'WordPress' for candidate in result['candidates'])) + def test_detects_mobirise_from_generator_and_assets(self): """Fingerprint should detect Mobirise landing-page builder output.""" @@ -330,6 +828,13 @@ def test_detects_5145_regional_cms_and_sitebuilder_catalog(self): '', {}, ), + ( + 'Evolution CMS', + 'cms', + '' + '
Powered by Evolution CMS
', + {}, + ), ( 'Duda', 'sitebuilder', @@ -403,6 +908,72 @@ def test_detects_5145_regional_cms_and_sitebuilder_catalog(self): self.assertEqual(result['name'], expected_name) self.assertGreaterEqual(result['confidence'], 70) + def test_detects_evolution_cms_from_not_installed_core_template(self): + """Fingerprint should detect Evolution CMS from its own install fallback text.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 503, + 'Evolution CMS is not currently installed or the configuration file cannot be found. ' + 'Do you want to install now?', + {}, + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse(404, 'Not Found', {}), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'cms') + self.assertEqual(result['name'], 'Evolution CMS') + self.assertGreaterEqual(result['confidence'], 70) + + def test_detects_evolution_cms_from_modx_evolution_body_and_manager_endpoint(self): + """Fingerprint should use MODX Evolution body text and /manager/ as corroborating evidence.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + 'MODX Evolution manager login powered by Evolution CMS.', + {}, + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse(404, 'Not Found', {}), + ('HEAD', 'http://example.com/manager/'): FakeResponse(200, '', {}), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'cms') + self.assertEqual(result['name'], 'Evolution CMS') + self.assertIn('modx evolution', [item['value'] for item in result['signals']]) + self.assertIn('/manager/', [item['value'] for item in result['signals']]) + + def test_does_not_detect_evolution_cms_from_generic_evo_word_only(self): + """Fingerprint should not classify generic EVO wording as Evolution CMS.""" + + config = FakeConfig() + base = 'http://example.com/' + responses = { + ('GET', base): FakeResponse( + 200, + '

Our product follows an evolutionary design process.

', + {}, + ), + ('GET', 'http://example.com{0}'.format(Fingerprint.NOT_FOUND_PROBE_PATH)): FakeResponse(404, 'Not Found', {}), + } + + detector = Fingerprint(config=config, client=self._make_client(config, responses)) + result = detector.detect() + + self.assertEqual(result['category'], 'custom') + self.assertEqual(result['name'], 'Unknown custom stack') + self.assertFalse(any(candidate['name'] == 'Evolution CMS' for candidate in result['candidates'])) + def test_does_not_promote_generic_diafan_link_without_meta_author(self): """Fingerprint should not promote DiafanCMS from a generic body mention only.""" diff --git a/tests/test_lib_browser_fingerprint_mogutacms.py b/tests/test_lib_browser_fingerprint_mogutacms.py new file mode 100644 index 00000000..e03ce156 --- /dev/null +++ b/tests/test_lib_browser_fingerprint_mogutacms.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +import unittest + +from src.lib.browser.fingerprint import Fingerprint + + +class TestFingerprintMogutaCMS(unittest.TestCase): + + def _detect_candidates(self, body='', generator=''): + fingerprint = Fingerprint(None, None) + fingerprint._apply_detection_rules( + body=body, + body_lower=str(body or '').lower(), + headers={}, + cookies=[], + generator=generator, + probe_statuses={}, + final_root_url='https://example.test/', + not_found_status=404, + not_found_body='not found', + not_found_headers={}, + ) + return fingerprint._build_candidates() + + def _candidate(self, candidates, name): + for candidate in candidates: + if candidate.get('name') == name: + return candidate + return None + + def test_detects_mogutacms_from_generator_meta(self): + candidates = self._detect_candidates(generator='Moguta.CMS') + candidate = self._candidate(candidates, 'MogutaCMS') + + self.assertIsNotNone(candidate) + self.assertEqual('ecommerce', candidate.get('category')) + self.assertGreaterEqual(candidate.get('score'), 8) + + def test_detects_mogutacms_from_powered_by_footer(self): + body = ''' +
+ Сайт работает на движке: + Moguta. CMS +
+ ''' + candidates = self._detect_candidates(body=body) + candidate = self._candidate(candidates, 'MogutaCMS') + + self.assertIsNotNone(candidate) + self.assertEqual('ecommerce', candidate.get('category')) + self.assertGreaterEqual(candidate.get('score'), 9) + + def test_detects_mogutacms_from_multiple_engine_paths(self): + body = ''' + + + ''' + candidates = self._detect_candidates(body=body) + candidate = self._candidate(candidates, 'MogutaCMS') + + self.assertIsNotNone(candidate) + self.assertEqual('ecommerce', candidate.get('category')) + self.assertGreaterEqual(candidate.get('score'), 8) + + def test_ignores_portfolio_or_comparison_text_without_site_signals(self): + body = ''' + MogutaCMS portfolio examples and ecommerce CMS comparison. + See moguta.ru for product information and implementation cases. + ''' + candidates = self._detect_candidates(body=body) + + self.assertIsNone(self._candidate(candidates, 'MogutaCMS')) + + def test_ignores_single_generic_mg_path_without_brand_context(self): + body = '' + candidates = self._detect_candidates(body=body) + + self.assertIsNone(self._candidate(candidates, 'MogutaCMS')) + + def test_uses_single_engine_path_only_as_brand_corroboration(self): + body = '' + fingerprint = Fingerprint(None, None) + + fingerprint._apply_detection_rules( + body=body, + body_lower=body.lower(), + headers={}, + cookies=[], + generator='Moguta.CMS', + probe_statuses={}, + final_root_url='https://example.test/', + not_found_status=404, + not_found_body='not found', + not_found_headers={}, + ) + + signals = getattr(fingerprint, '_Fingerprint__signals')['MogutaCMS'] + self.assertIn( + {'type': 'asset+brand', 'value': '/mg-templates/', 'weight': 4.0}, + signals, + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_lib_browser_fingerprint_rails.py b/tests/test_lib_browser_fingerprint_rails.py new file mode 100644 index 00000000..bea3b6e9 --- /dev/null +++ b/tests/test_lib_browser_fingerprint_rails.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- + +import unittest + +from src.lib.browser.fingerprint import Fingerprint + + +class _FingerprintConfig(object): + """Minimal browser config for isolated Rails fingerprint tests.""" + + DEFAULT_SCHEME = 'http://' + DEFAULT_HTTP_PORT = 80 + DEFAULT_SSL_PORT = 443 + + scheme = 'https://' + host = 'example.com' + port = 443 + prefix = '' + _method = 'HEAD' + + +class _Response(object): + """Small HTTP response stub used by the fingerprint detector.""" + + def __init__(self, status=200, headers=None, data=b''): + """ + Initialize a deterministic response object. + + :param int status: HTTP status code + :param dict|None headers: response headers + :param bytes data: response body + :return: None + """ + + self.status = status + self.headers = headers or {} + self.data = data + + +class _Client(object): + """Static client returning root and generic 404 responses.""" + + def __init__(self, root_body='', root_headers=None, not_found_body='not found'): + """ + Initialize the client fixture. + + :param str root_body: body returned for the target root URL + :param dict|None root_headers: headers returned for the target root URL + :param str not_found_body: body returned for non-root probes + :return: None + """ + + self.root_body = root_body + self.root_headers = root_headers or {} + self.not_found_body = not_found_body + + def request(self, url): + """ + Return root response or neutral 404 for probes. + + :param str url: requested URL + :return: response stub + :rtype: _Response + """ + + if str(url) == 'https://example.com/': + return _Response(status=200, headers=self.root_headers, data=self.root_body.encode('utf-8')) + + return _Response(status=404, headers={'Server': 'nginx'}, data=self.not_found_body.encode('utf-8')) + + +class FingerprintRailsTestCase(unittest.TestCase): + """Regression tests for conservative passive Ruby on Rails fingerprinting.""" + + def _detect(self, root_body='', root_headers=None, not_found_body='not found'): + """ + Run fingerprint detector against a deterministic client fixture. + + :param str root_body: root response body + :param dict|None root_headers: root response headers + :param str not_found_body: 404 probe response body + :return: fingerprint result + :rtype: dict + """ + + return Fingerprint( + _FingerprintConfig(), + _Client(root_body=root_body, root_headers=root_headers, not_found_body=not_found_body), + ).detect() + + def test_keeps_rails_session_cookie_detection(self): + result = self._detect(root_headers={'Set-Cookie': '_rails_session=abc; path=/; HttpOnly'}) + + self.assertEqual('Ruby on Rails', result['name']) + self.assertEqual('Ruby', result['runtime']['name']) + + def test_detects_exact_rails_csrf_meta(self): + body = ''' + + + + hello + ''' + + result = self._detect(root_body=body) + + self.assertEqual('Ruby on Rails', result['name']) + self.assertIn('csrf-param=authenticity_token|csrf-token', [item['value'] for item in result['signals']]) + + def test_keeps_generic_csrf_pair_as_low_confidence_rails_candidate(self): + body = ''' + + + + hello + ''' + fingerprint = Fingerprint(None, None) + + fingerprint._apply_detection_rules( + body=body, + body_lower=body.lower(), + headers={}, + cookies=[], + generator='', + probe_statuses={}, + final_root_url='https://example.test/', + not_found_status=404, + not_found_body='not found', + not_found_headers={}, + ) + + signals = getattr(fingerprint, '_Fingerprint__signals')['Ruby on Rails'] + self.assertIn( + {'type': 'markup', 'value': 'csrf-param|csrf-token', 'weight': 5.0}, + signals, + ) + + def test_detects_rails_ujs_only_with_csrf_corroboration(self): + body = ''' + + + + ''' + + result = self._detect(root_body=body) + + self.assertEqual('Ruby on Rails', result['name']) + self.assertIn('rails-ujs|turbo-rails', [item['value'] for item in result['signals']]) + + def test_detects_rails_asset_pipeline_only_with_corroboration(self): + body = ''' + + + + ''' + + result = self._detect(root_body=body) + + self.assertEqual('Ruby on Rails', result['name']) + self.assertIn('rails application asset', [item['value'] for item in result['signals']]) + + def test_detects_rails_exception_marker_from_root_body(self): + result = self._detect(root_body='

ActionController::RoutingError

Rails.root
') + + self.assertEqual('Ruby on Rails', result['name']) + self.assertIn('Rails exception marker', [item['value'] for item in result['signals']]) + + def test_detects_rails_exception_marker_from_not_found_body(self): + result = self._detect(not_found_body='

ActionView::Template::Error

Rails.root
') + + self.assertEqual('Ruby on Rails', result['name']) + self.assertIn('Rails exception marker', [item['value'] for item in result['signals']]) + + def test_does_not_detect_rails_from_rack_or_passenger_headers_alone(self): + result = self._detect(root_headers={'X-Runtime': '0.042', 'Server': 'Phusion Passenger'}) + + self.assertNotEqual('Ruby on Rails', result['name']) + + def test_does_not_detect_rails_from_ujs_or_assets_alone(self): + body = ''' + + + Delete + ''' + + result = self._detect(root_body=body) + + self.assertNotEqual('Ruby on Rails', result['name']) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_lib_browser_fingerprint_runtime_extra.py b/tests/test_lib_browser_fingerprint_runtime_extra.py index cf1486aa..3f2d5678 100644 --- a/tests/test_lib_browser_fingerprint_runtime_extra.py +++ b/tests/test_lib_browser_fingerprint_runtime_extra.py @@ -143,7 +143,7 @@ def test_fingerprint_evidence_output_deduplicates_values_only(self): fingerprint_instance.detect.return_value = fingerprint_result with patch('src.lib.browser.browser.Fingerprint', return_value=fingerprint_instance), \ - patch('src.lib.browser.browser.tpl.info') as info_mock: + patch('src.lib.browser.browser.tpl.debug') as info_mock: result = browser.fingerprint() self.assertEqual(result['signals'], fingerprint_result['signals']) diff --git a/tests/test_lib_browser_fingerprint_umi_cms.py b/tests/test_lib_browser_fingerprint_umi_cms.py new file mode 100644 index 00000000..d46a5366 --- /dev/null +++ b/tests/test_lib_browser_fingerprint_umi_cms.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- + +import unittest + +from src.lib.browser.fingerprint import Fingerprint + + +class _FingerprintConfig(object): + """Minimal browser config for isolated fingerprint tests.""" + + DEFAULT_SCHEME = 'http://' + DEFAULT_HTTP_PORT = 80 + DEFAULT_SSL_PORT = 443 + + scheme = 'https://' + host = 'example.com' + port = 443 + prefix = '' + _method = 'HEAD' + + +class _Response(object): + """Small HTTP response stub used by the fingerprint detector.""" + + def __init__(self, status=200, headers=None, data=b''): + """ + Initialize a deterministic response object. + + :param int status: HTTP status code + :param dict|None headers: response headers + :param bytes data: response body + :return: None + """ + + self.status = status + self.headers = headers or {} + self.data = data + + +class _Client(object): + """Static client returning root headers and generic 404 for probes.""" + + def __init__(self, root_headers): + """ + Initialize the client with root response headers. + + :param dict root_headers: headers returned for the target root URL + :return: None + """ + + self.root_headers = root_headers + + def request(self, url): + """ + Return the root response or a neutral 404 response for probes. + + :param str url: requested URL + :return: response stub + :rtype: _Response + """ + + if str(url) == 'https://example.com/': + return _Response( + status=200, + headers=self.root_headers, + data=b'plain page', + ) + + return _Response( + status=404, + headers={'Server': 'Apache/2'}, + data=b'not found', + ) + + +class FingerprintUmiCmsTestCase(unittest.TestCase): + """Regression tests for passive UMI.CMS header fingerprinting.""" + + def test_detects_umi_cms_from_x_generated_by_header(self): + result = Fingerprint( + _FingerprintConfig(), + _Client({'X-Generated-By': 'UMI.CMS', 'X-CMS-Version': '22', 'Server': 'Apache/2'}), + ).detect() + + self.assertEqual('UMI.CMS', result['name']) diff --git a/tests/test_lib_browser_shadow.py b/tests/test_lib_browser_shadow.py index 23a271a9..ca5ba745 100644 --- a/tests/test_lib_browser_shadow.py +++ b/tests/test_lib_browser_shadow.py @@ -66,6 +66,14 @@ def test_should_enable_only_when_shadow_sniffer_is_selected(self): self.assertFalse(ShadowProbe.is_enabled(SimpleNamespace(is_sniff=True, sniffers=['secret']))) self.assertTrue(ShadowProbe.is_enabled(SimpleNamespace(is_sniff=True, sniffers=['shadow']))) + def test_control_suffix_should_not_expose_tool_name(self): + """Should keep negative-control URLs neutral for target logs.""" + + self.assertNotIn('opendoor', ShadowProbe.CONTROL_SUFFIX.lower()) + self.assertNotIn('shadow', ShadowProbe.CONTROL_SUFFIX.lower()) + self.assertTrue(ShadowProbe.CONTROL_SUFFIX.startswith('.')) + self.assertTrue(ShadowProbe.CONTROL_SUFFIX.endswith('__')) + def test_should_identify_file_like_seed_urls(self): """Should probe source/config-like files and skip directories/binary assets.""" @@ -209,6 +217,58 @@ def test_should_ignore_unrelated_shadow_candidate(self): self.assertEqual(probe.findings, 0) + def test_should_suppress_shadow_burst_when_control_matches_fallback(self): + """Should suppress fallback-like shadow bursts without lowering real matching rules.""" + + base_body = b'dashboard
same dynamic fallback body
' + base = self.make_response(body=base_body) + seen_urls = [] + matches = [] + + def request(url): + seen_urls.append(url) + if url.endswith(ShadowProbe.CONTROL_SUFFIX): + return self.make_response(body=base_body.replace(b'dashboard', b'dashboard ')) + return self.make_response(body=base_body + b'') + + probe = ShadowProbe(request, lambda url, response, metadata: matches.append((url, metadata))) + with patch.object(probe, '_ShadowProbe__suffixes', ['.bak', '.old', '.tmp']): + self.assertEqual(probe.enqueue('https://example.com/register-site-admin.php', base, 'success'), 3) + probe.drain() + + self.assertEqual(seen_urls, ['https://example.com/register-site-admin.php{0}'.format(ShadowProbe.CONTROL_SUFFIX)]) + self.assertEqual(matches, []) + self.assertEqual(probe.submitted, 3) + self.assertEqual(probe.completed, 3) + self.assertEqual(probe.findings, 0) + + def test_should_keep_real_shadow_candidate_when_control_does_not_match(self): + """Should preserve real shadow findings when the negative control is not fallback-like.""" + + base_body = b'\n' * 8 + shadow_body = base_body + b'// backup-only marker\n' + base = self.make_response(body=base_body, content_type='text/plain') + matches = [] + + def request(url): + if url.endswith(ShadowProbe.CONTROL_SUFFIX): + return self.make_response(body=b'not found', status=404, content_type='text/plain') + if url.endswith('.bak'): + return self.make_response(body=shadow_body, content_type='text/plain') + return self.make_response(body=base_body, content_type='text/plain') + + probe = ShadowProbe(request, lambda url, response, metadata: matches.append((url, metadata))) + with patch.object(probe, '_ShadowProbe__suffixes', ['.bak', '.old']): + self.assertEqual(probe.enqueue('https://example.com/index.php', base, 'success'), 2) + probe.drain() + + self.assertEqual(len(matches), 1) + self.assertEqual(matches[0][0], 'https://example.com/index.php.bak') + self.assertEqual(matches[0][1]['shadow_detection']['reason'], 'content_diff') + self.assertEqual(probe.submitted, 2) + self.assertEqual(probe.completed, 2) + self.assertEqual(probe.findings, 1) + def test_should_skip_non_success_non_file_like_binary_and_shadow_buckets(self): """Should keep active probing limited to success 200 file-like responses.""" @@ -360,6 +420,90 @@ def test_debug_classification_renders_shadow_detection(self): rendered = [call.kwargs.get('msg') for call in debug_mock.call_args_list] self.assertTrue(any('Shadow detection:' in str(item) for item in rendered)) + def test_shadow_template_and_candidate_edge_branches(self): + """Shadow template helpers should reject malformed rendered paths.""" + + self.assertIsNone(ShadowProbe.build_template_path('/index.', '{path}2.{ext}')) + self.assertIsNone(ShadowProbe.build_template_path('/index.php', '{path}{bad}.{ext}')) + self.assertIsNone(ShadowProbe.build_template_path('/index.php', '{path}.{ext}')) + self.assertEqual(ShadowProbe.build_candidates('https://example.com/index.php', ['.bak', '.bak']), [ + ('https://example.com/index.php.bak', '.bak'), + ]) + + def test_shadow_control_candidate_and_fallback_edges(self): + """Shadow negative-control helpers should cover parse and mismatch guards.""" + + self.assertIsNone(ShadowProbe.build_control_candidate('http://[::1')) + self.assertIsNone(ShadowProbe.build_control_candidate('https://example.com/admin/')) + + identical = self.make_response(body=b'body', content_type='text/html') + base = ShadowProbe.response_signature(identical) + different_type = self.make_response(body=b'body', content_type='application/json') + self.assertFalse(ShadowProbe.is_fallback_like_control(base, different_type)) + + self.assertTrue(ShadowProbe.is_fallback_like_control(base, identical)) + self.assertFalse(ShadowProbe.is_fallback_like_control(None, identical)) + + def test_shadow_enqueue_control_candidate_guards(self): + """Shadow control enqueue should skip short, invalid and duplicate states.""" + + probe = ShadowProbe(MagicMock(), MagicMock()) + base_signature = {'hash': 'h', 'normalized': 'body', 'size': 10, 'content_type': 'text/html'} + enqueue_control = getattr(probe, '_ShadowProbe__enqueue_control_candidate') + + enqueue_control('https://example.com/index.php', base_signature, [ + ('https://example.com/index.php.bak', '.bak'), + ]) + self.assertEqual(getattr(probe, '_ShadowProbe__queue').qsize(), 0) + + enqueue_control('https://example.com/admin/', base_signature, [ + ('https://example.com/admin/.bak', '.bak'), + ('https://example.com/admin/.old', '.old'), + ]) + self.assertEqual(getattr(probe, '_ShadowProbe__queue').qsize(), 0) + + enqueue_control('https://example.com/index.php', base_signature, [ + ('https://example.com/index.php.bak', '.bak'), + ('https://example.com/index.php.old', '.old'), + ]) + enqueue_control('https://example.com/index.php', base_signature, [ + ('https://example.com/index.php.tmp', '.tmp'), + ('https://example.com/index.php.save', '.save'), + ]) + self.assertEqual(getattr(probe, '_ShadowProbe__queue').qsize(), 1) + + def test_shadow_process_control_task_delay_exception_and_state(self): + """Control probes should honor delay and mark allowed on request errors.""" + + probe = ShadowProbe(MagicMock(side_effect=RuntimeError('offline')), MagicMock(), delay=0.5) + base_signature = {'hash': 'h', 'normalized': 'body', 'size': 10, 'content_type': 'text/html'} + + with patch('src.lib.browser.shadow.time.sleep') as sleep_mock: + getattr(probe, '_ShadowProbe__process_control_task')( + 'https://example.com/index.php', + base_signature, + 'https://example.com/index.php{0}'.format(ShadowProbe.CONTROL_SUFFIX), + ) + + sleep_mock.assert_called_once_with(0.5) + self.assertFalse(getattr(probe, '_ShadowProbe__is_base_suppressed')('https://example.com/index.php')) + self.assertFalse(getattr(ShadowProbe, '_ShadowProbe__is_control_task')(None)) + + def test_shadow_enqueue_stops_when_probe_limit_is_reached_mid_batch(self): + """Shadow enqueue should stop cleanly once the total probe limit is reached mid-batch.""" + + request = MagicMock(return_value=self.make_response(body=b'different')) + probe = ShadowProbe(request, MagicMock()) + base = self.make_response(body=b'base stable body line\n' * 5) + + with patch.object(probe, '_ShadowProbe__suffixes', ['.bak', '.old', '.tmp']), \ + patch.object(ShadowProbe, 'MAX_TOTAL_PROBES', 1): + self.assertEqual(probe.enqueue('https://example.com/index.php', base, 'success'), 1) + probe.drain() + + self.assertLessEqual(probe.submitted, 1) + + class TestBrowserShadowIntegration(unittest.TestCase): """Browser integration tests for active shadow probe plumbing.""" diff --git a/tests/test_lib_browser_sniffer_runtime.py b/tests/test_lib_browser_sniffer_runtime.py index 41655b36..a1cbdf26 100644 --- a/tests/test_lib_browser_sniffer_runtime.py +++ b/tests/test_lib_browser_sniffer_runtime.py @@ -726,6 +726,113 @@ def test_warn_filtered_progress_mode_emits_when_sniffing_is_enabled(self): warning.assert_called_once_with(key='filtered_progress_notice') + def test_destructor_records_cleanup_errors_without_raising(self): + """Browser destructor should keep cleanup best-effort and record cleanup failures.""" + + browser = Browser.__new__(Browser) + browser._Browser__temp_workspace = SimpleNamespace(cleanup=MagicMock(side_effect=RuntimeError('cleanup boom'))) + + browser.__del__() + + self.assertEqual(browser._Browser__cleanup_error, 'cleanup boom') + + def test_diagnostics_table_expands_second_column_for_wide_title(self): + """Diagnostics table should expand its value column when the title is wider.""" + + table = Browser._Browser__format_diagnostics_table('Very wide diagnostics title', [('a', 'b')]) + + self.assertIn('Very wide diagnostics title', table) + self.assertIn('| a | b', table) + + def test_truncate_signal_values_handles_scalars_duplicates_and_limit(self): + """Fingerprint evidence helper should normalize scalar signals and preserve unique order.""" + + values = Browser._Browser__fingerprint_evidence_values([ + {'value': ' /admin '}, + '/admin', + '', + {'value': '/login'}, + '/debug', + ], limit=2) + + self.assertEqual(values, ['/admin', '/login']) + + def test_safe_progress_int_covers_bool_float_and_invalid_string(self): + """Progress coercion should stay deterministic for primitive and invalid values.""" + + self.assertEqual(Browser._Browser__safe_progress_int(True), 1) + self.assertEqual(Browser._Browser__safe_progress_int(3.8), 3) + self.assertEqual(Browser._Browser__safe_progress_int('bad', default=7), 7) + + def test_filtered_response_data_initializes_missing_raw_bucket(self): + """Filtered-response persistence should create the raw filtered_items list on demand.""" + + browser = self.make_browser() + browser._Browser__result.pop('filtered_items', None) + + browser._Browser__catch_filtered_response_data('https://example.com/noise', '10B', '200') + + self.assertEqual(browser._Browser__result['filtered_items'], [{ + 'url': 'https://example.com/noise', + 'size': '10B', + 'code': '200', + 'reason': 'response_filter', + }]) + + def test_loaded_session_complete_handles_empty_pending_invalid_and_complete_states(self): + """Loaded-session completion check should be defensive around checkpoint counters.""" + + browser = self.make_browser() + browser._Browser__session_snapshot = None + browser._Browser__pending_requests = {} + browser._Browser__processed_offset = 0 + browser._Browser__pool = SimpleNamespace(total_items_size=1) + + self.assertFalse(browser._Browser__is_loaded_session_complete()) + + browser._Browser__session_snapshot = {'version': 1} + browser._Browser__pending_requests = {'0::https://example.com/a': {}} + self.assertFalse(browser._Browser__is_loaded_session_complete()) + + browser._Browser__pending_requests = {} + browser._Browser__processed_offset = 'bad' + self.assertFalse(browser._Browser__is_loaded_session_complete()) + + browser._Browser__processed_offset = 2 + browser._Browser__pool = SimpleNamespace(total_items_size=2) + self.assertTrue(browser._Browser__is_loaded_session_complete()) + + def test_restore_session_state_initializes_report_and_filtered_items(self): + """Session restore should repair legacy result dictionaries and seed pending URL dedupe.""" + + browser = self.make_browser() + browser._Browser__pool = SimpleNamespace(total_items_size=1) + snapshot = { + 'result': {'total': {}, 'items': {}, 'filtered_items': 'legacy-invalid'}, + 'visitedRecursive': ['https://example.com/root'], + 'queuedRecursive': ['https://example.com/next'], + 'seen': ['0::https://example.com/done', 'legacy'], + 'pending': [ + {'url': 'https://example.com/todo', 'depth': '1'}, + {'url': None, 'depth': 0}, + ], + 'stats': { + 'processed': 3, + 'pre_request_skipped': 1, + 'transport_failures_skipped': 2, + 'active_time_seconds': 4.5, + 'total_items': 5, + }, + } + + browser._Browser__restore_session_state(snapshot) + + self.assertIn('report_items', browser._Browser__result) + self.assertEqual(browser._Browser__result['filtered_items'], []) + self.assertIn('https://example.com/done', browser._Browser__seen_scan_urls) + self.assertIn('https://example.com/todo', browser._Browser__seen_scan_urls) + self.assertEqual(browser._Browser__pool.total_items_size, 5) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_lib_browser_threadpool_worker.py b/tests/test_lib_browser_threadpool_worker.py index f1c38183..6ecd53a0 100644 --- a/tests/test_lib_browser_threadpool_worker.py +++ b/tests/test_lib_browser_threadpool_worker.py @@ -89,19 +89,37 @@ def test_threadpool_uses_default_stall_warning_interval_for_invalid_values(self) float(ThreadPool.JOIN_STALL_WARNING_SEC), ) - def test_add_calls_pause_on_keyboard_interrupt(self): - """ThreadPool.add() should open the pause menu when queue.put() is interrupted.""" + def test_add_retries_current_item_after_pause_continue_on_keyboard_interrupt(self): + """ThreadPool.add() should preserve the current item when pause resumes.""" with patch('src.lib.browser.threadpool.Worker', side_effect=lambda q, n, t: FakeWorker(q, n, t)): pool = ThreadPool(num_threads=1, total_items=5, timeout=0) queue_mock = getattr(pool, '_ThreadPool__queue') - with patch.object(queue_mock, 'put', side_effect=KeyboardInterrupt), \ + with patch.object(queue_mock, 'put', side_effect=[KeyboardInterrupt, None]) as put_mock, \ patch.object(pool, 'pause') as pause_mock: pool.add(lambda: None) pause_mock.assert_called_once_with() + self.assertEqual(put_mock.call_count, 2) + self.assertEqual(pool.submitted_size, 1) + + def test_add_keeps_item_unsubmitted_when_pause_aborts(self): + """ThreadPool.add() should not count an item when the pause prompt aborts.""" + + with patch('src.lib.browser.threadpool.Worker', side_effect=lambda q, n, t: FakeWorker(q, n, t)): + pool = ThreadPool(num_threads=1, total_items=5, timeout=0) + + queue_mock = getattr(pool, '_ThreadPool__queue') + + with patch.object(queue_mock, 'put', side_effect=KeyboardInterrupt), \ + patch.object(pool, 'pause', side_effect=KeyboardInterrupt) as pause_mock: + with self.assertRaises(KeyboardInterrupt): + pool.add(lambda: None) + + pause_mock.assert_called_once_with() + self.assertEqual(pool.submitted_size, 0) def test_threadpool_add_uses_submitted_counter_not_processed_items(self): """ThreadPool.add() should limit queue submissions using submitted_size.""" @@ -141,6 +159,7 @@ def test_pause_resumes_on_c_and_can_pause_workers(self): setattr(pool, '_ThreadPool__workers', [worker]) with patch('src.lib.browser.threadpool.tpl.info') as info_mock, \ + patch('src.lib.browser.threadpool.tpl.warning') as warning_mock, \ patch('src.lib.browser.threadpool.tpl.prompt', side_effect=['x', 'c']): pool.pause() @@ -148,6 +167,106 @@ def test_pause_resumes_on_c_and_can_pause_workers(self): self.assertTrue(worker.resume.called) self.assertTrue(pool.is_started) self.assertTrue(info_mock.called) + warning_mock.assert_called_once_with(key='unknown_pause_command') + + def test_pause_prompt_templates_are_concise_and_describe_existing_commands(self): + """Runtime pause templates should describe the existing continue/abort commands.""" + + from src.lib.tpl.config import Config + + self.assertEqual( + Config.templates['stop_threads'], + 'Pausing workers ({threads}). Active requests may finish first...', + ) + self.assertEqual( + Config.templates['option_prompt'], + 'Scan paused. Press Enter/[C] to continue or [E]/[Q] to abort scan: ', + ) + self.assertEqual( + Config.templates['unknown_pause_command'], + 'Unknown command. Use Enter/C to continue or E/Q to abort scan.', + ) + + def test_pause_prompt_waits_for_active_task_output_to_drain(self): + """ThreadPool.pause() should show the prompt after the last active task drains.""" + + events = [] + + class DrainingWorker: + def __init__(self): + self.active_reads = 0 + + def pause(self): + events.append('pause') + + def resume(self): + events.append('resume') + + @property + def active_task(self): + events.append('active') + self.active_reads += 1 + + if self.active_reads == 1: + return { + 'label': 'https://example.test/last-active', + 'started_at': 1.0, + } + + return None + + with patch('src.lib.browser.threadpool.Worker', side_effect=lambda q, n, t: FakeWorker(q, n, t)): + pool = ThreadPool(num_threads=1, total_items=5, timeout=0) + + setattr(pool, '_ThreadPool__workers', [DrainingWorker()]) + + def continue_prompt(key, newline=False): + events.append('prompt') + self.assertEqual(key, 'option_prompt') + self.assertTrue(newline) + return 'c' + + with patch('src.lib.browser.threadpool.tpl.info'), \ + patch('src.lib.browser.threadpool.tpl.prompt', side_effect=continue_prompt) as prompt_mock, \ + patch('src.lib.browser.threadpool.time.sleep') as sleep_mock: + pool.pause() + + self.assertLess(events.index('active'), events.index('prompt')) + self.assertIn('resume', events) + prompt_mock.assert_called_once_with(key='option_prompt', newline=True) + sleep_mock.assert_called_once_with(pool.PAUSE_PROMPT_DRAIN_POLL_SEC) + + def test_pause_prompt_drain_is_bounded_for_stuck_active_tasks(self): + """ThreadPool.pause() should not hide the prompt behind a stuck active request.""" + + class StickyWorker: + def pause(self): + pass + + def resume(self): + pass + + @property + def active_task(self): + return { + 'label': 'https://example.test/stuck', + 'started_at': 1.0, + } + + with patch('src.lib.browser.threadpool.Worker', side_effect=lambda q, n, t: FakeWorker(q, n, t)): + pool = ThreadPool(num_threads=1, total_items=5, timeout=0) + + setattr(pool, '_ThreadPool__workers', [StickyWorker()]) + pool.PAUSE_PROMPT_DRAIN_TIMEOUT_SEC = 0.0 + + with patch('src.lib.browser.threadpool.tpl.info'), \ + patch('src.lib.browser.threadpool.tpl.prompt', return_value='c') as prompt_mock, \ + patch('src.lib.browser.threadpool.time.sleep') as sleep_mock: + pool.pause() + + prompt_mock.assert_called_once_with(key='option_prompt', newline=True) + sleep_mock.assert_not_called() + self.assertTrue(pool.is_started) def test_pause_raises_keyboard_interrupt_on_e(self): """ThreadPool.pause() should raise KeyboardInterrupt on 'e'.""" @@ -528,7 +647,7 @@ def test_threadpool_pause_prompts_even_from_main_thread(self): patch('src.lib.browser.threadpool.tpl.info'): pool.pause() - prompt_mock.assert_called_once_with(key='option_prompt') + prompt_mock.assert_called_once_with(key='option_prompt', newline=True) self.assertTrue(pool.is_started) def test_threadpool_resume_is_noop_when_already_started(self): @@ -547,6 +666,35 @@ def test_threadpool_resume_is_noop_when_already_started(self): info_mock.assert_not_called() worker.resume.assert_not_called() + + def test_request_pause_marks_workers_and_join_opens_pause_prompt(self): + """ThreadPool.request_pause() should defer the interactive prompt to join().""" + + with patch('src.lib.browser.threadpool.Worker', side_effect=lambda q, n, t: FakeWorker(q, n, t)): + pool = ThreadPool(num_threads=1, total_items=1, timeout=0) + + worker = getattr(pool, '_ThreadPool__workers')[0] + queue = getattr(pool, '_ThreadPool__queue') + queue.put((lambda: None, (), {})) + + self.assertTrue(pool.request_pause()) + self.assertTrue(worker.paused) + self.assertFalse(pool.request_pause()) + + def wait_once(timeout=None): + queue.unfinished_tasks = 0 + return True + + def pause_once(): + setattr(pool, '_ThreadPool__pause_requested', False) + + with patch.object(queue.all_tasks_done, 'wait', side_effect=wait_once), \ + patch.object(pool, 'pause', side_effect=pause_once) as pause_mock, \ + patch('src.lib.browser.threadpool.time.monotonic', side_effect=[1.0, 2.0]): + pool.join() + + pause_mock.assert_called_once_with() + def test_threadpool_join_opens_pause_menu_on_keyboard_interrupt_and_continues(self): """ThreadPool.join() should pause/resume instead of bubbling Ctrl+C immediately.""" diff --git a/tests/test_lib_reader.py b/tests/test_lib_reader.py index 039fc443..567db837 100644 --- a/tests/test_lib_reader.py +++ b/tests/test_lib_reader.py @@ -255,6 +255,22 @@ def test_directories_line_uses_prefix_when_configured(self): self.assertEqual(line, 'http://example.com/admin/login.php') + def test_directories_line_normalizes_prefix_without_trailing_slash(self): + """Reader._directories__line() should treat bare prefix values as path segments.""" + + reader = self.create_reader(browser_config={'prefix': 'admin', 'list': 'directories'}) + line = reader._directories__line('login.php\n', {'scheme': 'http://', 'host': 'example.com', 'port': 80}) + + self.assertEqual(line, 'http://example.com/admin/login.php') + + def test_directories_line_normalizes_prefix_with_outer_slashes(self): + """Reader._directories__line() should avoid duplicated separators around prefixes.""" + + reader = self.create_reader(browser_config={'prefix': '/admin/', 'list': 'directories'}) + line = reader._directories__line('/login.php\n', {'scheme': 'http://', 'host': 'example.com', 'port': 80}) + + self.assertEqual(line, 'http://example.com/admin/login.php') + def test_directories_line_appends_non_default_port(self): """Reader._directories__line() should include a non-default port in the final URL.""" @@ -289,6 +305,12 @@ def test_subdomains_line_should_strip_www_when_host_context_is_not_prepared(self self.assertEqual(line, 'http://api.example.com:8080') + def test_normalize_directory_prefix_should_accept_none(self): + """Reader._normalize_directory_prefix() should treat None as no prefix.""" + + self.assertEqual(Reader._normalize_directory_prefix(None), '') + + def test_count_active_lines_should_wrap_filesystem_errors(self): """Reader.count_active_lines() should wrap filesystem count errors.""" diff --git a/tests/test_lib_reporter_txt_coverage.py b/tests/test_lib_reporter_txt_coverage.py index 974d2a82..e1521cb8 100644 --- a/tests/test_lib_reporter_txt_coverage.py +++ b/tests/test_lib_reporter_txt_coverage.py @@ -109,6 +109,35 @@ def test_should_process_legacy_url_items_when_report_items_are_missing(self): '\n' ) + def test_should_write_transport_failed_report_when_present(self): + """TextReportPlugin.process() should write transport_failed.txt outside finding buckets.""" + + data = { + 'items': {}, + 'transport_failed': [ + { + 'url': 'http://example.com/offline-admin', + 'code': '-', + 'size': '0B', + 'reason': 'no_response_after_retries', + } + ], + } + + with patch('src.lib.reporter.plugins.txt.filesystem.makedir', return_value=self.target_dir), \ + patch('src.lib.reporter.plugins.txt.filesystem.clear') as clear_mock: + plugin = TextReportPlugin(self.target, data, directory='/custom/reports') + with patch.object(plugin, 'record') as record_mock: + plugin.process() + + clear_mock.assert_called_once_with(self.target_dir, extension='.txt') + record_mock.assert_called_once_with( + self.target_dir, + 'transport_failed', + ['http://example.com/offline-admin - - - 0B'], + '\n' + ) + def test_should_process_empty_items_without_writing_report_files(self): """TextReportPlugin.process() should clear stale txt reports even when no items exist.""" diff --git a/tests/test_lib_tpl.py b/tests/test_lib_tpl.py index de2a16fe..ef4ee34e 100644 --- a/tests/test_lib_tpl.py +++ b/tests/test_lib_tpl.py @@ -92,6 +92,20 @@ def test_prompt_with_key(self): self.assertEqual(result, 'C') input_mock.assert_called_once() + def test_prompt_newline_should_write_complete_prompt_line_before_input(self): + """Tpl.prompt() should keep runtime menus on their own terminal line.""" + + with patch('src.lib.tpl.tpl.sys.finish_dynamic_line') as finish_mock, \ + patch('src.lib.tpl.tpl.sys.writeln') as writeln_mock, \ + patch('builtins.input', return_value='C') as input_mock: + result = Tpl.prompt(key='option_prompt', newline=True) + + self.assertEqual(result, 'C') + finish_mock.assert_called_once_with() + writeln_mock.assert_called_once() + self.assertTrue(writeln_mock.call_args[0][0]) + input_mock.assert_called_once_with('') + def test_prompt_exception(self): """Tpl.prompt() should wrap template lookup errors.""" diff --git a/tests/test_report_sizes.py b/tests/test_report_sizes.py index 3ec80d51..15416bf9 100644 --- a/tests/test_report_sizes.py +++ b/tests/test_report_sizes.py @@ -162,6 +162,30 @@ def test_json_report_contains_report_items_with_size_metadata(self): self.assertIn('"size": "9B"', content) self.assertIn('"url": "http://example.com/admin"', content) + def test_json_report_preserves_transport_failed_entries(self): + """JSON reports should preserve transport-failed diagnostics when present.""" + + data = dict(self.data) + data['transport_failed'] = [ + { + 'url': 'http://example.com/offline-admin', + 'size': '0B', + 'code': '-', + 'reason': 'no_response_after_retries', + } + ] + + plugin = JsonReportPlugin(self.target, data, directory=self.base_dir + os.path.sep) + plugin.process() + + report_file = os.path.join(self.base_dir, self.target, self.target + '.json') + with open(report_file, 'r', encoding='utf-8') as handler: + content = handler.read() + + self.assertIn('"transport_failed"', content) + self.assertIn('"reason": "no_response_after_retries"', content) + self.assertIn('"url": "http://example.com/offline-admin"', content) + def test_html_report_uses_detailed_report_items_and_builds_fallbacks(self): """HTML reports should receive report_items regardless of input payload shape."""