moriarty-project 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moriarty/__init__.py +5 -0
- moriarty/adapters/__init__.py +0 -0
- moriarty/agent/__init__.py +0 -0
- moriarty/assets/modules/.gitkeep +0 -0
- moriarty/assets/modules/asia/douban.yaml +19 -0
- moriarty/assets/modules/asia/kakao.yaml +19 -0
- moriarty/assets/modules/asia/line.yaml +19 -0
- moriarty/assets/modules/asia/mixi.yaml +19 -0
- moriarty/assets/modules/asia/naver.yaml +19 -0
- moriarty/assets/modules/asia/qq.yaml +19 -0
- moriarty/assets/modules/asia/vk.yaml +19 -0
- moriarty/assets/modules/asia/wechat.yaml +19 -0
- moriarty/assets/modules/asia/weibo.yaml +19 -0
- moriarty/assets/modules/asia/xiaohongshu.yaml +19 -0
- moriarty/assets/modules/behance.yaml +47 -0
- moriarty/assets/modules/business/crunchbase.yaml +27 -0
- moriarty/assets/modules/business/fiverr.yaml +32 -0
- moriarty/assets/modules/business/freelancer.yaml +27 -0
- moriarty/assets/modules/business/glassdoor.yaml +27 -0
- moriarty/assets/modules/business/guru.yaml +26 -0
- moriarty/assets/modules/business/indeed.yaml +25 -0
- moriarty/assets/modules/business/monster.yaml +25 -0
- moriarty/assets/modules/business/peopleperhour.yaml +26 -0
- moriarty/assets/modules/business/toptal.yaml +28 -0
- moriarty/assets/modules/business/upwork.yaml +27 -0
- moriarty/assets/modules/business/ziprecruiter.yaml +25 -0
- moriarty/assets/modules/content/buymeacoffee.yaml +27 -0
- moriarty/assets/modules/content/gumroad.yaml +27 -0
- moriarty/assets/modules/content/ko-fi.yaml +32 -0
- moriarty/assets/modules/content/onlyfans.yaml +27 -0
- moriarty/assets/modules/content/patreon.yaml +33 -0
- moriarty/assets/modules/content/substack.yaml +32 -0
- moriarty/assets/modules/creative/500px.yaml +31 -0
- moriarty/assets/modules/creative/artstation.yaml +33 -0
- moriarty/assets/modules/creative/deviantart.yaml +32 -0
- moriarty/assets/modules/creative/flickr.yaml +31 -0
- moriarty/assets/modules/creative/pexels.yaml +26 -0
- moriarty/assets/modules/creative/unsplash.yaml +26 -0
- moriarty/assets/modules/creative/vimeo.yaml +31 -0
- moriarty/assets/modules/crypto/binance.yaml +27 -0
- moriarty/assets/modules/crypto/bitcointalk.yaml +33 -0
- moriarty/assets/modules/crypto/coinbase.yaml +26 -0
- moriarty/assets/modules/crypto/etherscan.yaml +32 -0
- moriarty/assets/modules/crypto/foundation.yaml +28 -0
- moriarty/assets/modules/crypto/kraken.yaml +27 -0
- moriarty/assets/modules/crypto/mirror.yaml +27 -0
- moriarty/assets/modules/crypto/niftygateway.yaml +26 -0
- moriarty/assets/modules/crypto/opensea.yaml +32 -0
- moriarty/assets/modules/crypto/rarible.yaml +27 -0
- moriarty/assets/modules/crypto/superrare.yaml +29 -0
- moriarty/assets/modules/dating/bumble.yaml +25 -0
- moriarty/assets/modules/dating/grindr.yaml +27 -0
- moriarty/assets/modules/dating/happn.yaml +25 -0
- moriarty/assets/modules/dating/her.yaml +27 -0
- moriarty/assets/modules/dating/hinge.yaml +25 -0
- moriarty/assets/modules/dating/match.yaml +25 -0
- moriarty/assets/modules/dating/meetme.yaml +27 -0
- moriarty/assets/modules/dating/okcupid.yaml +25 -0
- moriarty/assets/modules/dating/pof.yaml +25 -0
- moriarty/assets/modules/dating/tinder.yaml +25 -0
- moriarty/assets/modules/dating-nsfw/adultfriendfinder.yaml +28 -0
- moriarty/assets/modules/dating-nsfw/ashley-madison.yaml +26 -0
- moriarty/assets/modules/design/adobe-portfolio.yaml +27 -0
- moriarty/assets/modules/design/carbonmade.yaml +27 -0
- moriarty/assets/modules/design/cgsociety.yaml +27 -0
- moriarty/assets/modules/design/coroflot.yaml +27 -0
- moriarty/assets/modules/design/figma.yaml +27 -0
- moriarty/assets/modules/design/sketch.yaml +26 -0
- moriarty/assets/modules/dev/bitbucket.yaml +35 -0
- moriarty/assets/modules/dev/codeforces.yaml +32 -0
- moriarty/assets/modules/dev/codepen.yaml +34 -0
- moriarty/assets/modules/dev/hackerone.yaml +32 -0
- moriarty/assets/modules/dev/hackthebox.yaml +27 -0
- moriarty/assets/modules/dev/huggingface.yaml +27 -0
- moriarty/assets/modules/dev/kaggle.yaml +32 -0
- moriarty/assets/modules/dev/leetcode.yaml +32 -0
- moriarty/assets/modules/dev/replit.yaml +31 -0
- moriarty/assets/modules/dribbble.yaml +53 -0
- moriarty/assets/modules/ecommerce/etsy.yaml +32 -0
- moriarty/assets/modules/education/duolingo.yaml +32 -0
- moriarty/assets/modules/education/edx.yaml +26 -0
- moriarty/assets/modules/education/khanacademy.yaml +26 -0
- moriarty/assets/modules/education/lynda.yaml +27 -0
- moriarty/assets/modules/education/memrise.yaml +27 -0
- moriarty/assets/modules/education/pluralsight.yaml +27 -0
- moriarty/assets/modules/education/skillshare.yaml +27 -0
- moriarty/assets/modules/education/udacity.yaml +27 -0
- moriarty/assets/modules/email/github_email.yaml +40 -0
- moriarty/assets/modules/email/gravatar.yaml +23 -0
- moriarty/assets/modules/europe/badoo.yaml +19 -0
- moriarty/assets/modules/europe/lovoo.yaml +19 -0
- moriarty/assets/modules/europe/myspace.yaml +19 -0
- moriarty/assets/modules/europe/netlog.yaml +19 -0
- moriarty/assets/modules/europe/ok.yaml +19 -0
- moriarty/assets/modules/europe/skyrock.yaml +19 -0
- moriarty/assets/modules/europe/studivz.yaml +19 -0
- moriarty/assets/modules/europe/tuenti.yaml +19 -0
- moriarty/assets/modules/europe/viadeo.yaml +19 -0
- moriarty/assets/modules/europe/xing.yaml +19 -0
- moriarty/assets/modules/fitness/fitbit.yaml +27 -0
- moriarty/assets/modules/fitness/garmin.yaml +27 -0
- moriarty/assets/modules/fitness/myfitnesspal.yaml +27 -0
- moriarty/assets/modules/fitness/strava.yaml +33 -0
- moriarty/assets/modules/fitness/zwift.yaml +28 -0
- moriarty/assets/modules/food/allrecipes.yaml +27 -0
- moriarty/assets/modules/food/tasty.yaml +27 -0
- moriarty/assets/modules/food/yelp.yaml +32 -0
- moriarty/assets/modules/food/zomato.yaml +28 -0
- moriarty/assets/modules/forums/4chan.yaml +26 -0
- moriarty/assets/modules/forums/8kun.yaml +26 -0
- moriarty/assets/modules/forums/9gag.yaml +26 -0
- moriarty/assets/modules/forums/discourse.yaml +26 -0
- moriarty/assets/modules/forums/disqus.yaml +31 -0
- moriarty/assets/modules/forums/hackernews.yaml +32 -0
- moriarty/assets/modules/forums/launchpad.yaml +27 -0
- moriarty/assets/modules/forums/phpbb.yaml +25 -0
- moriarty/assets/modules/forums/quora.yaml +32 -0
- moriarty/assets/modules/forums/serverfault.yaml +27 -0
- moriarty/assets/modules/forums/slashdot.yaml +28 -0
- moriarty/assets/modules/forums/stackexchange.yaml +32 -0
- moriarty/assets/modules/forums/superuser.yaml +27 -0
- moriarty/assets/modules/forums/vbulletin.yaml +25 -0
- moriarty/assets/modules/forums/xenforo.yaml +25 -0
- moriarty/assets/modules/forums-nsfw/kiwifarms.yaml +25 -0
- moriarty/assets/modules/forums-nsfw/lolcow.yaml +26 -0
- moriarty/assets/modules/gaming/apextracker.yaml +27 -0
- moriarty/assets/modules/gaming/battlenet.yaml +26 -0
- moriarty/assets/modules/gaming/chess.yaml +30 -0
- moriarty/assets/modules/gaming/discord-public.yaml +27 -0
- moriarty/assets/modules/gaming/dotabuff.yaml +32 -0
- moriarty/assets/modules/gaming/epicgames.yaml +25 -0
- moriarty/assets/modules/gaming/faceit.yaml +33 -0
- moriarty/assets/modules/gaming/fortnitetracker.yaml +32 -0
- moriarty/assets/modules/gaming/gog.yaml +26 -0
- moriarty/assets/modules/gaming/itch.yaml +32 -0
- moriarty/assets/modules/gaming/kongregate.yaml +25 -0
- moriarty/assets/modules/gaming/minecraft.yaml +31 -0
- moriarty/assets/modules/gaming/opgg.yaml +32 -0
- moriarty/assets/modules/gaming/origin.yaml +26 -0
- moriarty/assets/modules/gaming/playstation.yaml +30 -0
- moriarty/assets/modules/gaming/roblox.yaml +31 -0
- moriarty/assets/modules/gaming/xbox.yaml +25 -0
- moriarty/assets/modules/github.yaml +68 -0
- moriarty/assets/modules/gitlab.yaml +60 -0
- moriarty/assets/modules/instagram.yaml +48 -0
- moriarty/assets/modules/latam/fotolog.yaml +27 -0
- moriarty/assets/modules/latam/orkut.yaml +26 -0
- moriarty/assets/modules/latam/taringa.yaml +27 -0
- moriarty/assets/modules/learning/coursera.yaml +26 -0
- moriarty/assets/modules/learning/udemy.yaml +26 -0
- moriarty/assets/modules/linkedin.yaml +40 -0
- moriarty/assets/modules/marketplaces/depop.yaml +28 -0
- moriarty/assets/modules/marketplaces/ebay.yaml +32 -0
- moriarty/assets/modules/marketplaces/grailed.yaml +27 -0
- moriarty/assets/modules/marketplaces/mercari.yaml +26 -0
- moriarty/assets/modules/marketplaces/poshmark.yaml +27 -0
- moriarty/assets/modules/marketplaces/reverb.yaml +27 -0
- moriarty/assets/modules/marketplaces/vinted.yaml +28 -0
- moriarty/assets/modules/medium.yaml +44 -0
- moriarty/assets/modules/music/audiomack.yaml +26 -0
- moriarty/assets/modules/music/bandcamp.yaml +30 -0
- moriarty/assets/modules/music/beatport.yaml +28 -0
- moriarty/assets/modules/music/deezer.yaml +26 -0
- moriarty/assets/modules/music/discogs.yaml +32 -0
- moriarty/assets/modules/music/genius.yaml +26 -0
- moriarty/assets/modules/music/lastfm.yaml +30 -0
- moriarty/assets/modules/music/mixcloud.yaml +26 -0
- moriarty/assets/modules/music/reverbnation.yaml +31 -0
- moriarty/assets/modules/music/soundcloud.yaml +31 -0
- moriarty/assets/modules/music/spotify.yaml +26 -0
- moriarty/assets/modules/music/tidal.yaml +26 -0
- moriarty/assets/modules/nsfw/adultwork.yaml +27 -0
- moriarty/assets/modules/nsfw/bongacams.yaml +28 -0
- moriarty/assets/modules/nsfw/cam4.yaml +28 -0
- moriarty/assets/modules/nsfw/chaturbate.yaml +28 -0
- moriarty/assets/modules/nsfw/clips4sale.yaml +27 -0
- moriarty/assets/modules/nsfw/extralunchmoney.yaml +27 -0
- moriarty/assets/modules/nsfw/fansly.yaml +28 -0
- moriarty/assets/modules/nsfw/fetlife.yaml +28 -0
- moriarty/assets/modules/nsfw/iwantclips.yaml +27 -0
- moriarty/assets/modules/nsfw/justforfans.yaml +28 -0
- moriarty/assets/modules/nsfw/loyalfans.yaml +28 -0
- moriarty/assets/modules/nsfw/manyvids.yaml +27 -0
- moriarty/assets/modules/nsfw/myfreecams.yaml +28 -0
- moriarty/assets/modules/nsfw/niteflirt.yaml +26 -0
- moriarty/assets/modules/nsfw/pornhub.yaml +32 -0
- moriarty/assets/modules/nsfw/redtube.yaml +27 -0
- moriarty/assets/modules/nsfw/stripchat.yaml +28 -0
- moriarty/assets/modules/nsfw/xhamster.yaml +27 -0
- moriarty/assets/modules/nsfw/xvideos.yaml +27 -0
- moriarty/assets/modules/nsfw/youporn.yaml +27 -0
- moriarty/assets/modules/photography/eyeem.yaml +25 -0
- moriarty/assets/modules/photography/fotki.yaml +25 -0
- moriarty/assets/modules/photography/photobucket.yaml +26 -0
- moriarty/assets/modules/photography/smugmug.yaml +25 -0
- moriarty/assets/modules/photography/vsco.yaml +27 -0
- moriarty/assets/modules/pinterest.yaml +40 -0
- moriarty/assets/modules/podcasts/anchor.yaml +26 -0
- moriarty/assets/modules/podcasts/castbox.yaml +26 -0
- moriarty/assets/modules/podcasts/podbean.yaml +26 -0
- moriarty/assets/modules/professional/about.yaml +31 -0
- moriarty/assets/modules/professional/academia.yaml +27 -0
- moriarty/assets/modules/professional/angellist.yaml +27 -0
- moriarty/assets/modules/professional/calendly.yaml +26 -0
- moriarty/assets/modules/professional/issuu.yaml +27 -0
- moriarty/assets/modules/professional/mendeley.yaml +27 -0
- moriarty/assets/modules/professional/notion.yaml +27 -0
- moriarty/assets/modules/professional/orcid.yaml +27 -0
- moriarty/assets/modules/professional/producthunt.yaml +31 -0
- moriarty/assets/modules/professional/researchgate.yaml +32 -0
- moriarty/assets/modules/professional/scribd.yaml +27 -0
- moriarty/assets/modules/professional/slideshare.yaml +31 -0
- moriarty/assets/modules/professional/trello.yaml +26 -0
- moriarty/assets/modules/professional/typeform.yaml +27 -0
- moriarty/assets/modules/reddit.yaml +46 -0
- moriarty/assets/modules/regional/amino.yaml +27 -0
- moriarty/assets/modules/regional/ask-fm.yaml +32 -0
- moriarty/assets/modules/regional/babycenter.yaml +26 -0
- moriarty/assets/modules/regional/cafemom.yaml +27 -0
- moriarty/assets/modules/regional/care2.yaml +27 -0
- moriarty/assets/modules/regional/diaspora.yaml +26 -0
- moriarty/assets/modules/regional/ello.yaml +27 -0
- moriarty/assets/modules/regional/gaia.yaml +27 -0
- moriarty/assets/modules/regional/habbo.yaml +27 -0
- moriarty/assets/modules/regional/imvu.yaml +27 -0
- moriarty/assets/modules/regional/lemmy.yaml +27 -0
- moriarty/assets/modules/regional/peertube.yaml +26 -0
- moriarty/assets/modules/regional/pixelfed.yaml +27 -0
- moriarty/assets/modules/regional/plurk.yaml +26 -0
- moriarty/assets/modules/regional/recroom.yaml +27 -0
- moriarty/assets/modules/regional/secondlife.yaml +26 -0
- moriarty/assets/modules/regional/vine-archive.yaml +27 -0
- moriarty/assets/modules/regional/vrchat.yaml +27 -0
- moriarty/assets/modules/regional/weheartit.yaml +27 -0
- moriarty/assets/modules/social/anilist.yaml +27 -0
- moriarty/assets/modules/social/beacons.yaml +26 -0
- moriarty/assets/modules/social/blogger.yaml +27 -0
- moriarty/assets/modules/social/crunchyroll.yaml +27 -0
- moriarty/assets/modules/social/discord.yaml +27 -0
- moriarty/assets/modules/social/dreamwidth.yaml +26 -0
- moriarty/assets/modules/social/facebook.yaml +34 -0
- moriarty/assets/modules/social/goodreads.yaml +32 -0
- moriarty/assets/modules/social/imdb.yaml +27 -0
- moriarty/assets/modules/social/kitsu.yaml +27 -0
- moriarty/assets/modules/social/letterboxd.yaml +32 -0
- moriarty/assets/modules/social/linktree.yaml +26 -0
- moriarty/assets/modules/social/livejournal.yaml +27 -0
- moriarty/assets/modules/social/mastodon.yaml +30 -0
- moriarty/assets/modules/social/minds.yaml +25 -0
- moriarty/assets/modules/social/myanimelist.yaml +32 -0
- moriarty/assets/modules/social/ravelry.yaml +27 -0
- moriarty/assets/modules/social/snapchat.yaml +25 -0
- moriarty/assets/modules/social/telegram.yaml +35 -0
- moriarty/assets/modules/social/tiktok.yaml +35 -0
- moriarty/assets/modules/social/trakt.yaml +28 -0
- moriarty/assets/modules/social/wattpad.yaml +32 -0
- moriarty/assets/modules/social/wordpress-com.yaml +26 -0
- moriarty/assets/modules/sports/espn.yaml +26 -0
- moriarty/assets/modules/sports/untappd.yaml +32 -0
- moriarty/assets/modules/stackoverflow.yaml +47 -0
- moriarty/assets/modules/steam.yaml +47 -0
- moriarty/assets/modules/streaming/caffeine.yaml +25 -0
- moriarty/assets/modules/streaming/dlive.yaml +27 -0
- moriarty/assets/modules/streaming/trovo.yaml +25 -0
- moriarty/assets/modules/travel/airbnb.yaml +26 -0
- moriarty/assets/modules/travel/booking.yaml +26 -0
- moriarty/assets/modules/travel/couchsurfing.yaml +27 -0
- moriarty/assets/modules/travel/tripadvisor.yaml +32 -0
- moriarty/assets/modules/tumblr.yaml +40 -0
- moriarty/assets/modules/twitch.yaml +48 -0
- moriarty/assets/modules/twitter.yaml +39 -0
- moriarty/assets/modules/youtube.yaml +42 -0
- moriarty/assets/templates/cves/CVE-2017-5638.yaml +27 -0
- moriarty/assets/templates/cves/CVE-2018-7600.yaml +30 -0
- moriarty/assets/templates/cves/CVE-2019-11510.yaml +27 -0
- moriarty/assets/templates/cves/CVE-2019-19781.yaml +28 -0
- moriarty/assets/templates/cves/CVE-2020-14882.yaml +28 -0
- moriarty/assets/templates/cves/CVE-2020-14883.yaml +29 -0
- moriarty/assets/templates/cves/CVE-2020-3452.yaml +28 -0
- moriarty/assets/templates/cves/CVE-2020-5902.yaml +28 -0
- moriarty/assets/templates/cves/CVE-2021-21972.yaml +31 -0
- moriarty/assets/templates/cves/CVE-2021-21985.yaml +28 -0
- moriarty/assets/templates/cves/CVE-2021-26084.yaml +30 -0
- moriarty/assets/templates/cves/CVE-2021-41773.yaml +25 -0
- moriarty/assets/templates/cves/CVE-2021-42013.yaml +28 -0
- moriarty/assets/templates/cves/CVE-2021-44228.yaml +27 -0
- moriarty/assets/templates/cves/CVE-2022-0185.yaml +21 -0
- moriarty/assets/templates/cves/CVE-2022-1388.yaml +36 -0
- moriarty/assets/templates/cves/CVE-2022-22954.yaml +28 -0
- moriarty/assets/templates/cves/CVE-2022-22965.yaml +31 -0
- moriarty/assets/templates/cves/CVE-2022-26134.yaml +27 -0
- moriarty/assets/templates/cves/CVE-2023-22515.yaml +27 -0
- moriarty/assets/templates/cves/CVE-2023-22527.yaml +29 -0
- moriarty/assets/templates/cves/CVE-2023-23752.yaml +33 -0
- moriarty/assets/templates/cves/CVE-2023-27350.yaml +27 -0
- moriarty/assets/templates/cves/CVE-2023-2868.yaml +27 -0
- moriarty/assets/templates/cves/CVE-2023-34362.yaml +27 -0
- moriarty/assets/templates/cves/CVE-2023-3519.yaml +28 -0
- moriarty/assets/templates/cves/CVE-2023-4966.yaml +27 -0
- moriarty/assets/templates/default-logins/admin-weak.yaml +40 -0
- moriarty/assets/templates/default-logins/wordpress-default.yaml +38 -0
- moriarty/assets/templates/exposures/aws-credentials.yaml +35 -0
- moriarty/assets/templates/exposures/backup-files.yaml +36 -0
- moriarty/assets/templates/exposures/database-files.yaml +34 -0
- moriarty/assets/templates/exposures/docker-exposed.yaml +31 -0
- moriarty/assets/templates/exposures/env-exposed.yaml +41 -0
- moriarty/assets/templates/exposures/git-exposed.yaml +41 -0
- moriarty/assets/templates/exposures/phpinfo.yaml +36 -0
- moriarty/assets/templates/exposures/svn-exposed.yaml +28 -0
- moriarty/assets/templates/fuzzing/api-endpoints.yaml +39 -0
- moriarty/assets/templates/fuzzing/common-files.yaml +37 -0
- moriarty/assets/templates/fuzzing/open-redirect-fuzz.yaml +35 -0
- moriarty/assets/templates/fuzzing/xss-search-fuzz.yaml +29 -0
- moriarty/assets/templates/git-config.yaml +18 -0
- moriarty/assets/templates/misconfigurations/cors-misconfiguration.yaml +30 -0
- moriarty/assets/templates/misconfigurations/debug-enabled.yaml +29 -0
- moriarty/assets/templates/misconfigurations/directory-listing.yaml +33 -0
- moriarty/assets/templates/misconfigurations/jwt-none-algo.yaml +30 -0
- moriarty/assets/templates/misconfigurations/ssl-tls-weak.yaml +23 -0
- moriarty/assets/templates/vulnerabilities/lfi-basic.yaml +31 -0
- moriarty/assets/templates/vulnerabilities/open-redirect.yaml +31 -0
- moriarty/assets/templates/vulnerabilities/rce-basic.yaml +34 -0
- moriarty/assets/templates/vulnerabilities/sqli-error.yaml +39 -0
- moriarty/assets/templates/vulnerabilities/ssrf-basic.yaml +31 -0
- moriarty/assets/templates/vulnerabilities/xss-reflected.yaml +38 -0
- moriarty/assets/templates/vulnerabilities/xxe-basic.yaml +30 -0
- moriarty/assets/wordlists/subdomains-1000.txt +1063 -0
- moriarty/cli/__init__.py +3 -0
- moriarty/cli/app.py +120 -0
- moriarty/cli/async_utils.py +19 -0
- moriarty/cli/dns.py +83 -0
- moriarty/cli/domain_cmd.py +572 -0
- moriarty/cli/email.py +383 -0
- moriarty/cli/email_investigate.py +224 -0
- moriarty/cli/intelligence.py +329 -0
- moriarty/cli/output.py +62 -0
- moriarty/cli/rdap.py +94 -0
- moriarty/cli/state.py +38 -0
- moriarty/cli/tls.py +91 -0
- moriarty/cli/user.py +227 -0
- moriarty/core/cache_backend.py +223 -0
- moriarty/core/config_manager.py +303 -0
- moriarty/correlator/__init__.py +0 -0
- moriarty/data/__init__.py +81 -0
- moriarty/data/ioc/__init__.py +142 -0
- moriarty/data/ioc/matcher.py +254 -0
- moriarty/data/ioc/types.py +267 -0
- moriarty/data/local_intelligence.py +507 -0
- moriarty/data/signature_loaders/__init__.py +103 -0
- moriarty/data/signature_loaders/base.py +54 -0
- moriarty/data/signature_loaders/ioc_feed.py +356 -0
- moriarty/data/signature_loaders/wappalyzer.py +112 -0
- moriarty/dsl/__init__.py +0 -0
- moriarty/dsl/loader.py +99 -0
- moriarty/dsl/schema.py +47 -0
- moriarty/export/__init__.py +0 -0
- moriarty/intelligence/__init__.py +27 -0
- moriarty/intelligence/__main__.py +150 -0
- moriarty/intelligence/config.py +395 -0
- moriarty/intelligence/ioc.py +267 -0
- moriarty/intelligence/signatures.py +550 -0
- moriarty/intelligence/storage.py +501 -0
- moriarty/interop/__init__.py +0 -0
- moriarty/logging/__init__.py +0 -0
- moriarty/logging/config.py +47 -0
- moriarty/models/__init__.py +16 -0
- moriarty/models/assertion.py +24 -0
- moriarty/models/entity.py +22 -0
- moriarty/models/evidence.py +37 -0
- moriarty/models/relation.py +24 -0
- moriarty/models/types.py +28 -0
- moriarty/modules/__init__.py +0 -0
- moriarty/modules/avatar_hash.py +184 -0
- moriarty/modules/directory_fuzzer.py +322 -0
- moriarty/modules/dns_scan.py +40 -0
- moriarty/modules/domain_scanner.py +620 -0
- moriarty/modules/email_check.py +98 -0
- moriarty/modules/email_investigate.py +267 -0
- moriarty/modules/email_security.py +274 -0
- moriarty/modules/googlemaps_lookup.py +106 -0
- moriarty/modules/headless_executor.py +201 -0
- moriarty/modules/orchestrator.py +60 -0
- moriarty/modules/passive_recon.py +444 -0
- moriarty/modules/phone_extractor.py +151 -0
- moriarty/modules/pipeline_orchestrator.py +726 -0
- moriarty/modules/port_scanner.py +129 -0
- moriarty/modules/rdap.py +61 -0
- moriarty/modules/rdap_extended.py +188 -0
- moriarty/modules/stealth_mode.py +610 -0
- moriarty/modules/subdomain_discovery.py +595 -0
- moriarty/modules/technology_profiler.py +361 -0
- moriarty/modules/template_executor.py +239 -0
- moriarty/modules/template_scanner.py +1048 -0
- moriarty/modules/tls_scan.py +46 -0
- moriarty/modules/tls_validator.py +188 -0
- moriarty/modules/vuln_scanner.py +483 -0
- moriarty/modules/waf_detector.py +585 -0
- moriarty/modules/wayback_discovery.py +234 -0
- moriarty/modules/web_crawler.py +163 -0
- moriarty/net/__init__.py +0 -0
- moriarty/net/dns_cache.py +175 -0
- moriarty/net/dns_client.py +188 -0
- moriarty/net/rdap_client.py +52 -0
- moriarty/net/smtp_client.py +114 -0
- moriarty/net/tls_client.py +111 -0
- moriarty/parsers/__init__.py +0 -0
- moriarty/parsers/html_parser.py +136 -0
- moriarty/tests/__init__.py +0 -0
- moriarty/tests/test_email_service.py +17 -0
- moriarty/tests/test_models.py +46 -0
- moriarty/tests/test_orchestrator.py +30 -0
- moriarty/tests/test_tls_client.py +18 -0
- moriarty_project-0.1.6.dist-info/METADATA +388 -0
- moriarty_project-0.1.6.dist-info/RECORD +418 -0
- moriarty_project-0.1.6.dist-info/WHEEL +4 -0
- moriarty_project-0.1.6.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,1048 @@
|
|
1
|
+
"""Template Scanner - Sistema estilo Nuclei para detecção de vulnerabilidades."""
|
2
|
+
import asyncio
|
3
|
+
import itertools
|
4
|
+
import json
|
5
|
+
import random
|
6
|
+
import re
|
7
|
+
from dataclasses import dataclass, field
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, TYPE_CHECKING
|
10
|
+
from urllib.parse import parse_qs, urlencode, urlparse, urlsplit, urlunsplit
|
11
|
+
|
12
|
+
import httpx
|
13
|
+
import structlog
|
14
|
+
import yaml
|
15
|
+
from rich.console import Console
|
16
|
+
from rich.progress import Progress
|
17
|
+
|
18
|
+
if TYPE_CHECKING: # pragma: no cover - apenas para type hints
|
19
|
+
from moriarty.modules.stealth_mode import StealthMode
|
20
|
+
|
21
|
+
logger = structlog.get_logger(__name__)
|
22
|
+
console = Console()
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class TemplateFinding:
|
27
|
+
"""Resultado de template scan."""
|
28
|
+
template_id: str
|
29
|
+
name: str
|
30
|
+
severity: str
|
31
|
+
description: str
|
32
|
+
matched_at: str
|
33
|
+
extracted_data: Dict[str, Any]
|
34
|
+
|
35
|
+
|
36
|
+
@dataclass
|
37
|
+
class Template:
|
38
|
+
"""Template de vulnerabilidade."""
|
39
|
+
id: str
|
40
|
+
info: Dict[str, Any]
|
41
|
+
requests: List[Dict[str, Any]]
|
42
|
+
matchers: List[Dict[str, Any]]
|
43
|
+
extractors: Optional[List[Dict[str, Any]]] = None
|
44
|
+
matchers_condition: str = "or"
|
45
|
+
tags: Set[str] = field(default_factory=set)
|
46
|
+
|
47
|
+
|
48
|
+
class TemplateScanner:
|
49
|
+
"""
|
50
|
+
Scanner baseado em templates YAML (estilo Nuclei).
|
51
|
+
|
52
|
+
Suporta:
|
53
|
+
- HTTP requests
|
54
|
+
- Matchers (status, word, regex, dsl)
|
55
|
+
- Extractors (regex, kval, json)
|
56
|
+
- Variables
|
57
|
+
- Payloads
|
58
|
+
- Raw requests
|
59
|
+
"""
|
60
|
+
|
61
|
+
FUZZ_PAYLOADS: Dict[str, List[str]] = {
|
62
|
+
"sqli": ["' OR '1'='1", "' UNION SELECT NULL--", "1; DROP TABLE users"],
|
63
|
+
"xss": ["<script>alert(1)</script>", '" onmouseover="alert(1)"', "'><img src=x onerror=alert(1)>"],
|
64
|
+
"lfi": ["../../../../etc/passwd", "..\\..\\..\\..\\windows\\win.ini"],
|
65
|
+
"cmd": [";id", "| whoami", "$(whoami)", "&& dir"],
|
66
|
+
"path": ["..;/", "..%2f"],
|
67
|
+
}
|
68
|
+
|
69
|
+
def __init__(
|
70
|
+
self,
|
71
|
+
target: str,
|
72
|
+
templates_path: Optional[str] = None,
|
73
|
+
severity_filter: Optional[List[str]] = None,
|
74
|
+
threads: int = 20,
|
75
|
+
timeout: float = 10.0,
|
76
|
+
tag_filter: Optional[List[str]] = None,
|
77
|
+
stealth: Optional["StealthMode"] = None,
|
78
|
+
):
|
79
|
+
self.target = target
|
80
|
+
self.templates_path = templates_path or self._get_default_templates_path()
|
81
|
+
self.severity_filter = severity_filter
|
82
|
+
self.threads = threads
|
83
|
+
self.timeout = timeout
|
84
|
+
self.tag_filter = {tag.lower() for tag in tag_filter} if tag_filter else None
|
85
|
+
self.stealth = stealth
|
86
|
+
self.findings: List[TemplateFinding] = []
|
87
|
+
self._finding_cache: Set[Tuple[str, str]] = set()
|
88
|
+
self.template_ids: Set[str] = set()
|
89
|
+
|
90
|
+
normalized_target = self._ensure_scheme(target)
|
91
|
+
parsed = urlparse(normalized_target)
|
92
|
+
self.scheme = parsed.scheme or "https"
|
93
|
+
self.host = parsed.netloc or parsed.path
|
94
|
+
self.base_url = f"{self.scheme}://{self.host}"
|
95
|
+
|
96
|
+
# Mantém alvo original para logging
|
97
|
+
self.target = normalized_target
|
98
|
+
|
99
|
+
def _stealth_headers(self) -> Dict[str, str]:
|
100
|
+
if not self.stealth:
|
101
|
+
return {}
|
102
|
+
headers = self.stealth.get_random_headers()
|
103
|
+
headers.setdefault("User-Agent", headers.get("User-Agent", "Mozilla/5.0 (Moriarty Recon)"))
|
104
|
+
headers.setdefault("Accept", "*/*")
|
105
|
+
return headers
|
106
|
+
|
107
|
+
async def _stealth_delay(self) -> None:
|
108
|
+
if not self.stealth:
|
109
|
+
return
|
110
|
+
config = getattr(self.stealth, "config", None)
|
111
|
+
if not config or not getattr(config, "timing_randomization", False):
|
112
|
+
return
|
113
|
+
await asyncio.sleep(random.uniform(0.05, 0.2) * max(1, self.stealth.level))
|
114
|
+
|
115
|
+
def _get_default_templates_path(self) -> str:
|
116
|
+
"""Retorna path padrão de templates."""
|
117
|
+
return str(Path(__file__).parent.parent / "assets" / "templates")
|
118
|
+
|
119
|
+
def _ensure_scheme(self, target: str) -> str:
|
120
|
+
"""Garante que o alvo possua esquema HTTP/S."""
|
121
|
+
if target.startswith("http://") or target.startswith("https://"):
|
122
|
+
return target
|
123
|
+
return f"https://{target}"
|
124
|
+
|
125
|
+
async def scan(self) -> List[TemplateFinding]:
|
126
|
+
"""Executa scan usando templates."""
|
127
|
+
logger.info("template.scan.start", target=self.target, templates_path=self.templates_path)
|
128
|
+
|
129
|
+
# Carrega templates
|
130
|
+
templates = self._load_templates()
|
131
|
+
|
132
|
+
# Filtra por severidade
|
133
|
+
if self.severity_filter:
|
134
|
+
templates = [t for t in templates if t.info.get("severity") in self.severity_filter]
|
135
|
+
|
136
|
+
if self.tag_filter:
|
137
|
+
filtered = [t for t in templates if t.tags and (t.tags & self.tag_filter)]
|
138
|
+
if filtered:
|
139
|
+
templates = filtered
|
140
|
+
else:
|
141
|
+
logger.debug("template.scan.tag_filter_empty", tags=sorted(self.tag_filter))
|
142
|
+
|
143
|
+
console.print(f"[cyan]📝 Loaded {len(templates)} templates[/cyan]\n")
|
144
|
+
|
145
|
+
# Executa templates
|
146
|
+
async with httpx.AsyncClient(timeout=self.timeout, follow_redirects=True) as client:
|
147
|
+
semaphore = asyncio.Semaphore(self.threads)
|
148
|
+
|
149
|
+
tasks = [
|
150
|
+
self._execute_template(client, semaphore, template)
|
151
|
+
for template in templates
|
152
|
+
]
|
153
|
+
|
154
|
+
with Progress() as progress:
|
155
|
+
task_id = progress.add_task("[cyan]Scanning...", total=len(tasks))
|
156
|
+
|
157
|
+
for coro in asyncio.as_completed(tasks):
|
158
|
+
await coro
|
159
|
+
progress.advance(task_id)
|
160
|
+
|
161
|
+
logger.info("template.scan.complete", findings=len(self.findings))
|
162
|
+
return self.findings
|
163
|
+
|
164
|
+
def _load_templates(self) -> List[Template]:
|
165
|
+
"""Carrega templates de diretório."""
|
166
|
+
templates = []
|
167
|
+
templates_dir = Path(self.templates_path)
|
168
|
+
|
169
|
+
if not templates_dir.exists():
|
170
|
+
logger.warning("template.load.not_found", path=self.templates_path)
|
171
|
+
# Cria templates de exemplo
|
172
|
+
self._create_example_templates()
|
173
|
+
return self._load_templates()
|
174
|
+
|
175
|
+
self.template_ids.clear()
|
176
|
+
|
177
|
+
# Busca arquivos .yaml recursivamente
|
178
|
+
for yaml_file in templates_dir.rglob("*.yaml"):
|
179
|
+
try:
|
180
|
+
with open(yaml_file, 'r') as f:
|
181
|
+
data = yaml.safe_load(f) or {}
|
182
|
+
|
183
|
+
if not isinstance(data, dict):
|
184
|
+
logger.warning("template.load.invalid", file=str(yaml_file))
|
185
|
+
continue
|
186
|
+
|
187
|
+
if not self._validate_template(data, yaml_file):
|
188
|
+
continue
|
189
|
+
|
190
|
+
template_id = data.get("id", yaml_file.stem)
|
191
|
+
if template_id in self.template_ids:
|
192
|
+
logger.warning("template.load.duplicate", template=template_id, file=str(yaml_file))
|
193
|
+
continue
|
194
|
+
|
195
|
+
matchers_condition = (
|
196
|
+
data.get("matchers-condition")
|
197
|
+
or data.get("matchers_condition")
|
198
|
+
or "or"
|
199
|
+
).lower()
|
200
|
+
|
201
|
+
requests = data.get("requests", data.get("http", [])) or []
|
202
|
+
if not isinstance(requests, list):
|
203
|
+
requests = [requests]
|
204
|
+
|
205
|
+
normalized_requests = [
|
206
|
+
self._normalize_request(request)
|
207
|
+
for request in requests
|
208
|
+
if isinstance(request, dict)
|
209
|
+
]
|
210
|
+
|
211
|
+
info = data.get("info", {})
|
212
|
+
raw_tags = info.get("tags", [])
|
213
|
+
if isinstance(raw_tags, str):
|
214
|
+
tags = {raw_tags.lower()}
|
215
|
+
else:
|
216
|
+
tags = {str(tag).lower() for tag in raw_tags}
|
217
|
+
|
218
|
+
template = Template(
|
219
|
+
id=template_id,
|
220
|
+
info=info,
|
221
|
+
requests=normalized_requests,
|
222
|
+
matchers=data.get("matchers", []),
|
223
|
+
extractors=data.get("extractors", []),
|
224
|
+
matchers_condition=matchers_condition,
|
225
|
+
tags=tags,
|
226
|
+
)
|
227
|
+
|
228
|
+
templates.append(template)
|
229
|
+
self.template_ids.add(template_id)
|
230
|
+
|
231
|
+
except Exception as e:
|
232
|
+
logger.warning("template.load.error", file=str(yaml_file), error=str(e))
|
233
|
+
|
234
|
+
logger.info("template.load.complete", count=len(templates))
|
235
|
+
return templates
|
236
|
+
|
237
|
+
def _normalize_request(self, request_def: Dict[str, Any]) -> Dict[str, Any]:
|
238
|
+
"""Normaliza campos comuns de requests."""
|
239
|
+
normalized = dict(request_def)
|
240
|
+
|
241
|
+
path = normalized.get("path")
|
242
|
+
if isinstance(path, str):
|
243
|
+
normalized["path"] = [path]
|
244
|
+
|
245
|
+
raw = normalized.get("raw")
|
246
|
+
if isinstance(raw, str):
|
247
|
+
normalized["raw"] = [raw]
|
248
|
+
|
249
|
+
matchers_condition = (
|
250
|
+
normalized.get("matchers-condition")
|
251
|
+
or normalized.get("matchers_condition")
|
252
|
+
)
|
253
|
+
if matchers_condition:
|
254
|
+
normalized["matchers_condition"] = matchers_condition
|
255
|
+
|
256
|
+
return normalized
|
257
|
+
|
258
|
+
def _validate_template(self, data: Dict[str, Any], file_path: Path) -> bool:
|
259
|
+
"""Valida estrutura mínima de um template."""
|
260
|
+
errors = []
|
261
|
+
|
262
|
+
template_id = data.get("id")
|
263
|
+
if not template_id:
|
264
|
+
errors.append("missing id")
|
265
|
+
|
266
|
+
info = data.get("info", {})
|
267
|
+
severity = info.get("severity")
|
268
|
+
allowed_severities = {"critical", "high", "medium", "low", "info"}
|
269
|
+
if severity and severity.lower() not in allowed_severities:
|
270
|
+
errors.append(f"invalid severity '{severity}'")
|
271
|
+
|
272
|
+
requests = data.get("requests", data.get("http", [])) or []
|
273
|
+
if not isinstance(requests, list) or not requests:
|
274
|
+
errors.append("missing requests")
|
275
|
+
else:
|
276
|
+
for index, request in enumerate(requests):
|
277
|
+
if not isinstance(request, dict):
|
278
|
+
errors.append(f"request[{index}] is not a mapping")
|
279
|
+
continue
|
280
|
+
has_path = bool(request.get("path"))
|
281
|
+
has_raw = bool(request.get("raw"))
|
282
|
+
if not has_path and not has_raw:
|
283
|
+
errors.append(f"request[{index}] missing path/raw")
|
284
|
+
|
285
|
+
if errors:
|
286
|
+
logger.warning(
|
287
|
+
"template.validation.failed",
|
288
|
+
file=str(file_path),
|
289
|
+
errors=errors,
|
290
|
+
)
|
291
|
+
return False
|
292
|
+
|
293
|
+
return True
|
294
|
+
|
295
|
+
async def _execute_template(
|
296
|
+
self,
|
297
|
+
client: httpx.AsyncClient,
|
298
|
+
semaphore: asyncio.Semaphore,
|
299
|
+
template: Template,
|
300
|
+
):
|
301
|
+
"""Executa um template."""
|
302
|
+
async with semaphore:
|
303
|
+
try:
|
304
|
+
for request_def in template.requests:
|
305
|
+
matchers = request_def.get("matchers") or template.matchers
|
306
|
+
extractors = request_def.get("extractors") or template.extractors or []
|
307
|
+
matchers_condition = (
|
308
|
+
request_def.get("matchers_condition")
|
309
|
+
or request_def.get("matchers-condition")
|
310
|
+
or template.matchers_condition
|
311
|
+
or "or"
|
312
|
+
).lower()
|
313
|
+
stop_on_match = bool(
|
314
|
+
request_def.get("stop-at-first-match")
|
315
|
+
or request_def.get("stop_at_first_match")
|
316
|
+
)
|
317
|
+
|
318
|
+
payload_maps = self._build_payload_maps(request_def)
|
319
|
+
raw_blocks = request_def.get("raw") or []
|
320
|
+
match_found = False
|
321
|
+
|
322
|
+
if raw_blocks:
|
323
|
+
for payload_map in payload_maps:
|
324
|
+
for raw in raw_blocks:
|
325
|
+
raw_payload = self._apply_payload(raw, payload_map)
|
326
|
+
method, url, headers, body = self._parse_raw_request(raw_payload)
|
327
|
+
|
328
|
+
request_kwargs = {}
|
329
|
+
if body is not None:
|
330
|
+
request_kwargs["content"] = body.encode() if isinstance(body, str) else body
|
331
|
+
|
332
|
+
merged_headers = {**self._stealth_headers(), **(headers or {})}
|
333
|
+
await self._stealth_delay()
|
334
|
+
response = await client.request(
|
335
|
+
method,
|
336
|
+
url,
|
337
|
+
headers=merged_headers,
|
338
|
+
**request_kwargs,
|
339
|
+
)
|
340
|
+
|
341
|
+
match_found = self._process_response(
|
342
|
+
template,
|
343
|
+
url,
|
344
|
+
response,
|
345
|
+
matchers,
|
346
|
+
matchers_condition,
|
347
|
+
extractors,
|
348
|
+
) or match_found
|
349
|
+
|
350
|
+
if match_found and stop_on_match:
|
351
|
+
return
|
352
|
+
|
353
|
+
# Se requests RAW foram processados, pula para próximo request
|
354
|
+
continue
|
355
|
+
|
356
|
+
paths = request_def.get("path") or ["/"]
|
357
|
+
if isinstance(paths, str):
|
358
|
+
paths = [paths]
|
359
|
+
|
360
|
+
method = request_def.get("method", "GET")
|
361
|
+
headers_template = request_def.get("headers", {})
|
362
|
+
body_template = request_def.get("body")
|
363
|
+
|
364
|
+
for payload_map in payload_maps:
|
365
|
+
for path in paths:
|
366
|
+
final_path = self._apply_payload(path, payload_map)
|
367
|
+
url = self._build_url(final_path)
|
368
|
+
|
369
|
+
headers = self._prepare_headers(headers_template, payload_map)
|
370
|
+
headers = {**self._stealth_headers(), **headers}
|
371
|
+
body = self._prepare_body(body_template, payload_map)
|
372
|
+
request_kwargs = self._build_request_kwargs(body, request_def)
|
373
|
+
|
374
|
+
await self._stealth_delay()
|
375
|
+
response = await client.request(
|
376
|
+
method,
|
377
|
+
url,
|
378
|
+
headers=headers,
|
379
|
+
**request_kwargs,
|
380
|
+
)
|
381
|
+
|
382
|
+
match_found = self._process_response(
|
383
|
+
template,
|
384
|
+
url,
|
385
|
+
response,
|
386
|
+
matchers,
|
387
|
+
matchers_condition,
|
388
|
+
extractors,
|
389
|
+
) or match_found
|
390
|
+
|
391
|
+
if match_found and stop_on_match:
|
392
|
+
break
|
393
|
+
|
394
|
+
fuzz_spec = request_def.get("fuzz")
|
395
|
+
if fuzz_spec:
|
396
|
+
match_found = await self._run_query_fuzz(
|
397
|
+
client,
|
398
|
+
template,
|
399
|
+
url,
|
400
|
+
headers,
|
401
|
+
method,
|
402
|
+
request_kwargs,
|
403
|
+
fuzz_spec,
|
404
|
+
matchers,
|
405
|
+
matchers_condition,
|
406
|
+
extractors,
|
407
|
+
int(request_def.get("fuzz_max", 30)),
|
408
|
+
stop_on_match,
|
409
|
+
match_found,
|
410
|
+
)
|
411
|
+
if match_found and stop_on_match:
|
412
|
+
break
|
413
|
+
|
414
|
+
if match_found and stop_on_match:
|
415
|
+
break
|
416
|
+
|
417
|
+
if match_found and stop_on_match:
|
418
|
+
break
|
419
|
+
|
420
|
+
if match_found and stop_on_match:
|
421
|
+
break
|
422
|
+
|
423
|
+
except Exception as e:
|
424
|
+
logger.debug("template.execute.error", template=template.id, error=str(e))
|
425
|
+
|
426
|
+
def _build_url(self, path: str) -> str:
|
427
|
+
"""Constrói URL completa."""
|
428
|
+
path = self._resolve_placeholders(path)
|
429
|
+
|
430
|
+
if path.startswith("http://") or path.startswith("https://"):
|
431
|
+
return path
|
432
|
+
|
433
|
+
if not path.startswith("/"):
|
434
|
+
path = f"/{path}"
|
435
|
+
|
436
|
+
return f"{self.base_url}{path}"
|
437
|
+
|
438
|
+
def _check_matchers(
|
439
|
+
self,
|
440
|
+
matchers: List[Dict[str, Any]],
|
441
|
+
response: httpx.Response,
|
442
|
+
matchers_condition: str = "or",
|
443
|
+
) -> bool:
|
444
|
+
"""Verifica se matchers são satisfeitos."""
|
445
|
+
if not matchers:
|
446
|
+
return False
|
447
|
+
|
448
|
+
results: List[bool] = []
|
449
|
+
|
450
|
+
for matcher in matchers:
|
451
|
+
matcher_type = matcher.get("type", "word").lower()
|
452
|
+
condition = matcher.get("condition", "or").lower()
|
453
|
+
negative = bool(matcher.get("negative"))
|
454
|
+
result = False
|
455
|
+
|
456
|
+
if matcher_type == "status":
|
457
|
+
status_codes = matcher.get("status") or matcher.get("codes") or []
|
458
|
+
result = response.status_code in status_codes
|
459
|
+
|
460
|
+
elif matcher_type == "word":
|
461
|
+
words = matcher.get("words", [])
|
462
|
+
part = matcher.get("part", "body")
|
463
|
+
content = self._get_response_part(response, part)
|
464
|
+
matches = [word.lower() in content.lower() for word in words]
|
465
|
+
result = all(matches) if condition == "and" else any(matches)
|
466
|
+
|
467
|
+
elif matcher_type == "regex":
|
468
|
+
regexes = matcher.get("regex", [])
|
469
|
+
part = matcher.get("part", "body")
|
470
|
+
content = self._get_response_part(response, part)
|
471
|
+
matches = [bool(re.search(regex, content, re.IGNORECASE)) for regex in regexes]
|
472
|
+
result = all(matches) if condition == "and" else any(matches)
|
473
|
+
|
474
|
+
elif matcher_type == "dsl":
|
475
|
+
expressions = matcher.get("dsl", [])
|
476
|
+
result = self._evaluate_dsl(expressions, response)
|
477
|
+
|
478
|
+
if negative:
|
479
|
+
result = not result
|
480
|
+
|
481
|
+
results.append(result)
|
482
|
+
|
483
|
+
if not results:
|
484
|
+
return False
|
485
|
+
|
486
|
+
if matchers_condition == "and":
|
487
|
+
return all(results)
|
488
|
+
return any(results)
|
489
|
+
|
490
|
+
def _process_response(
|
491
|
+
self,
|
492
|
+
template: Template,
|
493
|
+
url: str,
|
494
|
+
response: httpx.Response,
|
495
|
+
matchers: List[Dict[str, Any]],
|
496
|
+
matchers_condition: str,
|
497
|
+
extractors: List[Dict[str, Any]],
|
498
|
+
) -> bool:
|
499
|
+
"""Processa response, aplicando matchers e registrando findings."""
|
500
|
+
if not matchers:
|
501
|
+
return False
|
502
|
+
|
503
|
+
if not self._check_matchers(matchers, response, matchers_condition):
|
504
|
+
return False
|
505
|
+
|
506
|
+
extracted = self._extract_data(extractors, response) if extractors else {}
|
507
|
+
cache_key = (template.id, url)
|
508
|
+
if cache_key in self._finding_cache:
|
509
|
+
return True
|
510
|
+
|
511
|
+
self._finding_cache.add(cache_key)
|
512
|
+
|
513
|
+
finding = TemplateFinding(
|
514
|
+
template_id=template.id,
|
515
|
+
name=template.info.get("name", template.id),
|
516
|
+
severity=template.info.get("severity", "info"),
|
517
|
+
description=template.info.get("description", ""),
|
518
|
+
matched_at=url,
|
519
|
+
extracted_data=extracted,
|
520
|
+
)
|
521
|
+
|
522
|
+
self.findings.append(finding)
|
523
|
+
|
524
|
+
logger.info(
|
525
|
+
"template.match",
|
526
|
+
template=template.id,
|
527
|
+
severity=finding.severity,
|
528
|
+
url=url,
|
529
|
+
)
|
530
|
+
|
531
|
+
colors = {
|
532
|
+
"critical": "red",
|
533
|
+
"high": "yellow",
|
534
|
+
"medium": "blue",
|
535
|
+
"low": "green",
|
536
|
+
"info": "dim",
|
537
|
+
}
|
538
|
+
color = colors.get(finding.severity, "white")
|
539
|
+
console.print(f"[{color}]✓ {finding.name} [{finding.severity.upper()}][/{color}]")
|
540
|
+
|
541
|
+
return True
|
542
|
+
|
543
|
+
def _build_payload_maps(self, request_def: Dict[str, Any]) -> List[Dict[str, Any]]:
|
544
|
+
"""Gera combinações de payloads para placeholders."""
|
545
|
+
payloads = request_def.get("payloads") or {}
|
546
|
+
auto_fuzz = request_def.get("auto_fuzz")
|
547
|
+
if isinstance(auto_fuzz, dict):
|
548
|
+
for placeholder, entries in auto_fuzz.items():
|
549
|
+
values = self._resolve_fuzz_values(entries if isinstance(entries, list) else [entries])
|
550
|
+
if values:
|
551
|
+
payloads.setdefault(placeholder, values)
|
552
|
+
elif isinstance(auto_fuzz, list):
|
553
|
+
for idx, category in enumerate(auto_fuzz, start=1):
|
554
|
+
values = self._resolve_fuzz_values([category])
|
555
|
+
if values:
|
556
|
+
payloads.setdefault(f"fuzz_{idx}", values)
|
557
|
+
if not payloads:
|
558
|
+
return [{}]
|
559
|
+
|
560
|
+
keys = list(payloads.keys())
|
561
|
+
value_lists = [payloads[key] for key in keys]
|
562
|
+
combinations = []
|
563
|
+
max_combos = int(request_def.get("max_payload_combinations", 256))
|
564
|
+
|
565
|
+
for index, combo in enumerate(itertools.product(*value_lists)):
|
566
|
+
if index >= max_combos:
|
567
|
+
logger.debug(
|
568
|
+
"template.payloads.limited",
|
569
|
+
template=request_def.get("id"),
|
570
|
+
max=max_combos,
|
571
|
+
)
|
572
|
+
break
|
573
|
+
combinations.append(dict(zip(keys, combo)))
|
574
|
+
|
575
|
+
return combinations or [{}]
|
576
|
+
|
577
|
+
def _apply_payload(self, value: Any, payload_map: Dict[str, Any]) -> Any:
|
578
|
+
"""Aplica placeholders e payloads em strings/listas/dicts."""
|
579
|
+
if value is None:
|
580
|
+
return None
|
581
|
+
|
582
|
+
if isinstance(value, str):
|
583
|
+
result = value
|
584
|
+
for key, payload in payload_map.items():
|
585
|
+
result = result.replace(f"{{{{{key}}}}}", str(payload))
|
586
|
+
return self._resolve_placeholders(result)
|
587
|
+
|
588
|
+
if isinstance(value, list):
|
589
|
+
return [self._apply_payload(item, payload_map) for item in value]
|
590
|
+
|
591
|
+
if isinstance(value, dict):
|
592
|
+
return {k: self._apply_payload(v, payload_map) for k, v in value.items()}
|
593
|
+
|
594
|
+
return value
|
595
|
+
|
596
|
+
def _resolve_fuzz_values(self, entries: List[str]) -> List[str]:
|
597
|
+
values: List[str] = []
|
598
|
+
for entry in entries:
|
599
|
+
if entry in self.FUZZ_PAYLOADS:
|
600
|
+
values.extend(self.FUZZ_PAYLOADS[entry])
|
601
|
+
else:
|
602
|
+
values.append(entry)
|
603
|
+
seen: Set[str] = set()
|
604
|
+
unique: List[str] = []
|
605
|
+
for value in values:
|
606
|
+
if value not in seen:
|
607
|
+
seen.add(value)
|
608
|
+
unique.append(value)
|
609
|
+
return unique
|
610
|
+
|
611
|
+
def _normalize_fuzz_spec(self, fuzz_spec: Any) -> Dict[str, List[str]]:
|
612
|
+
if not isinstance(fuzz_spec, dict):
|
613
|
+
return {}
|
614
|
+
normalized: Dict[str, List[str]] = {}
|
615
|
+
for param, entries in fuzz_spec.items():
|
616
|
+
if isinstance(entries, str):
|
617
|
+
entries = [entries]
|
618
|
+
if not isinstance(entries, list):
|
619
|
+
continue
|
620
|
+
values = self._resolve_fuzz_values(entries)
|
621
|
+
if values:
|
622
|
+
normalized[param] = values
|
623
|
+
return normalized
|
624
|
+
|
625
|
+
async def _run_query_fuzz(
|
626
|
+
self,
|
627
|
+
client: httpx.AsyncClient,
|
628
|
+
template: Template,
|
629
|
+
url: str,
|
630
|
+
headers: Dict[str, Any],
|
631
|
+
method: str,
|
632
|
+
request_kwargs: Dict[str, Any],
|
633
|
+
fuzz_spec: Any,
|
634
|
+
matchers: List[Dict[str, Any]],
|
635
|
+
matchers_condition: str,
|
636
|
+
extractors: List[Dict[str, Any]],
|
637
|
+
max_mutations: int,
|
638
|
+
stop_on_match: bool,
|
639
|
+
current_match: bool,
|
640
|
+
) -> bool:
|
641
|
+
method_upper = method.upper()
|
642
|
+
if method_upper not in {"GET", "HEAD", "DELETE"}:
|
643
|
+
return current_match
|
644
|
+
|
645
|
+
fuzz_map = self._normalize_fuzz_spec(fuzz_spec)
|
646
|
+
if not fuzz_map:
|
647
|
+
return current_match
|
648
|
+
|
649
|
+
parsed = urlsplit(url)
|
650
|
+
base_params = parse_qs(parsed.query, keep_blank_values=True)
|
651
|
+
for key in fuzz_map:
|
652
|
+
base_params.setdefault(key, [""])
|
653
|
+
|
654
|
+
keys = list(fuzz_map.keys())
|
655
|
+
values_lists = [fuzz_map[key] for key in keys]
|
656
|
+
|
657
|
+
mutations = 0
|
658
|
+
for combo in itertools.product(*values_lists):
|
659
|
+
if max_mutations > 0 and mutations >= max_mutations:
|
660
|
+
logger.debug("template.fuzz.limit", template=template.id, limit=max_mutations)
|
661
|
+
break
|
662
|
+
|
663
|
+
params = {key: values[:] for key, values in base_params.items()}
|
664
|
+
for key, payload in zip(keys, combo):
|
665
|
+
params[key] = [payload]
|
666
|
+
|
667
|
+
new_query = urlencode(params, doseq=True)
|
668
|
+
fuzz_url = urlunsplit((parsed.scheme, parsed.netloc, parsed.path, new_query, parsed.fragment))
|
669
|
+
|
670
|
+
request_headers = {**self._stealth_headers(), **(headers or {})}
|
671
|
+
await self._stealth_delay()
|
672
|
+
response = await client.request(
|
673
|
+
method_upper,
|
674
|
+
fuzz_url,
|
675
|
+
headers=request_headers,
|
676
|
+
**request_kwargs,
|
677
|
+
)
|
678
|
+
|
679
|
+
current_match = self._process_response(
|
680
|
+
template,
|
681
|
+
fuzz_url,
|
682
|
+
response,
|
683
|
+
matchers,
|
684
|
+
matchers_condition,
|
685
|
+
extractors,
|
686
|
+
) or current_match
|
687
|
+
|
688
|
+
mutations += 1
|
689
|
+
if current_match and stop_on_match:
|
690
|
+
break
|
691
|
+
|
692
|
+
return current_match
|
693
|
+
|
694
|
+
def _resolve_placeholders(self, value: str) -> str:
|
695
|
+
"""Resolve placeholders padrão (BaseURL, Hostname, Target)."""
|
696
|
+
replacements = {
|
697
|
+
"{{BaseURL}}": self.base_url,
|
698
|
+
"{{base_url}}": self.base_url,
|
699
|
+
"{{RootURL}}": self.base_url,
|
700
|
+
"{{root_url}}": self.base_url,
|
701
|
+
"{{Hostname}}": self.host,
|
702
|
+
"{{hostname}}": self.host,
|
703
|
+
"{{Target}}": self.host,
|
704
|
+
"{{target}}": self.host,
|
705
|
+
}
|
706
|
+
|
707
|
+
result = value
|
708
|
+
for placeholder, replacement in replacements.items():
|
709
|
+
result = result.replace(placeholder, replacement)
|
710
|
+
|
711
|
+
return result
|
712
|
+
|
713
|
+
def _prepare_headers(
|
714
|
+
self,
|
715
|
+
headers_template: Dict[str, Any],
|
716
|
+
payload_map: Dict[str, Any],
|
717
|
+
) -> Dict[str, str]:
|
718
|
+
"""Prepara headers aplicando payloads e defaults."""
|
719
|
+
headers = {
|
720
|
+
key: self._apply_payload(str(value), payload_map)
|
721
|
+
for key, value in (headers_template or {}).items()
|
722
|
+
}
|
723
|
+
|
724
|
+
headers.setdefault("Host", self.host)
|
725
|
+
return headers
|
726
|
+
|
727
|
+
def _prepare_body(self, body_template: Any, payload_map: Dict[str, Any]) -> Any:
|
728
|
+
"""Prepara body aplicando payloads."""
|
729
|
+
if body_template is None:
|
730
|
+
return None
|
731
|
+
return self._apply_payload(body_template, payload_map)
|
732
|
+
|
733
|
+
def _build_request_kwargs(self, body: Any, request_def: Dict[str, Any]) -> Dict[str, Any]:
|
734
|
+
"""Monta kwargs para httpx.request."""
|
735
|
+
kwargs: Dict[str, Any] = {}
|
736
|
+
|
737
|
+
if body is None:
|
738
|
+
return kwargs
|
739
|
+
|
740
|
+
if isinstance(body, dict):
|
741
|
+
if request_def.get("json", True):
|
742
|
+
kwargs["json"] = body
|
743
|
+
else:
|
744
|
+
kwargs["data"] = body
|
745
|
+
else:
|
746
|
+
kwargs["content"] = body.encode() if isinstance(body, str) else body
|
747
|
+
|
748
|
+
return kwargs
|
749
|
+
|
750
|
+
def _parse_raw_request(self, raw_request: str) -> Tuple[str, str, Dict[str, str], Optional[str]]:
|
751
|
+
"""Converte request RAW estilo Nuclei em componentes httpx."""
|
752
|
+
lines = raw_request.splitlines()
|
753
|
+
if not lines:
|
754
|
+
raise ValueError("raw request vazio")
|
755
|
+
|
756
|
+
request_line = lines[0].strip()
|
757
|
+
try:
|
758
|
+
method, path, _ = request_line.split()
|
759
|
+
except ValueError as exc:
|
760
|
+
raise ValueError(f"linha de requisição inválida: {request_line}") from exc
|
761
|
+
|
762
|
+
headers: Dict[str, str] = {}
|
763
|
+
body_lines: List[str] = []
|
764
|
+
parsing_headers = True
|
765
|
+
|
766
|
+
for line in lines[1:]:
|
767
|
+
if parsing_headers and not line.strip():
|
768
|
+
parsing_headers = False
|
769
|
+
continue
|
770
|
+
|
771
|
+
if parsing_headers:
|
772
|
+
if ":" in line:
|
773
|
+
key, value = line.split(":", 1)
|
774
|
+
headers[key.strip()] = value.strip()
|
775
|
+
else:
|
776
|
+
body_lines.append(line)
|
777
|
+
|
778
|
+
body = "\n".join(body_lines) if body_lines else None
|
779
|
+
|
780
|
+
url = path
|
781
|
+
if not url.startswith("http://") and not url.startswith("https://"):
|
782
|
+
url = self._build_url(url)
|
783
|
+
|
784
|
+
headers.setdefault("Host", self.host)
|
785
|
+
|
786
|
+
return method.upper(), url, headers, body
|
787
|
+
|
788
|
+
def _evaluate_dsl(self, expressions: List[str], response: httpx.Response) -> bool:
|
789
|
+
"""Avalia expressões DSL simples."""
|
790
|
+
if not expressions:
|
791
|
+
return False
|
792
|
+
|
793
|
+
body = response.text or ""
|
794
|
+
headers = {k.lower(): v for k, v in response.headers.items()}
|
795
|
+
status_code = response.status_code
|
796
|
+
json_cache: Any = None
|
797
|
+
|
798
|
+
def _normalize(source: Any) -> str:
|
799
|
+
if source is None:
|
800
|
+
return ""
|
801
|
+
return str(source)
|
802
|
+
|
803
|
+
def _contains(source: Any, needle: str) -> bool:
|
804
|
+
return needle.lower() in _normalize(source).lower()
|
805
|
+
|
806
|
+
def _icontains(source: Any, needle: str) -> bool:
|
807
|
+
return _contains(source, needle)
|
808
|
+
|
809
|
+
def _startswith(source: Any, prefix: str) -> bool:
|
810
|
+
return _normalize(source).lower().startswith(prefix.lower())
|
811
|
+
|
812
|
+
def _endswith(source: Any, suffix: str) -> bool:
|
813
|
+
return _normalize(source).lower().endswith(suffix.lower())
|
814
|
+
|
815
|
+
def _equals(left: Any, right: Any) -> bool:
|
816
|
+
return _normalize(left).lower() == _normalize(right).lower()
|
817
|
+
|
818
|
+
def _regex(pattern: str, source: Optional[str] = None) -> bool:
|
819
|
+
target = source if source is not None else body
|
820
|
+
return bool(re.search(pattern, target or "", re.IGNORECASE))
|
821
|
+
|
822
|
+
def _header(name: str) -> Optional[str]:
|
823
|
+
return headers.get(name.lower())
|
824
|
+
|
825
|
+
def _count(source: Any, needle: str) -> int:
|
826
|
+
return _normalize(source).lower().count(needle.lower())
|
827
|
+
|
828
|
+
def _status_in(*codes: int) -> bool:
|
829
|
+
return status_code in set(int(code) for code in codes)
|
830
|
+
|
831
|
+
def _status_between(start: int, end: int) -> bool:
|
832
|
+
return start <= status_code <= end
|
833
|
+
|
834
|
+
def _json(path: str, default: Any = None) -> Any:
|
835
|
+
nonlocal json_cache
|
836
|
+
if json_cache is None:
|
837
|
+
try:
|
838
|
+
json_cache = json.loads(body)
|
839
|
+
except Exception:
|
840
|
+
json_cache = {}
|
841
|
+
|
842
|
+
current = json_cache
|
843
|
+
if not path:
|
844
|
+
return current
|
845
|
+
for part in path.split('.'):
|
846
|
+
if isinstance(current, dict) and part in current:
|
847
|
+
current = current[part]
|
848
|
+
elif isinstance(current, list):
|
849
|
+
try:
|
850
|
+
idx = int(part)
|
851
|
+
current = current[idx]
|
852
|
+
except (ValueError, IndexError):
|
853
|
+
return default
|
854
|
+
else:
|
855
|
+
return default
|
856
|
+
return current
|
857
|
+
|
858
|
+
def _present(value: Any) -> bool:
|
859
|
+
if value is None:
|
860
|
+
return False
|
861
|
+
if isinstance(value, str):
|
862
|
+
return bool(value.strip())
|
863
|
+
if isinstance(value, (list, dict, set, tuple)):
|
864
|
+
return len(value) > 0
|
865
|
+
return True
|
866
|
+
|
867
|
+
env = {
|
868
|
+
"body": body,
|
869
|
+
"headers": headers,
|
870
|
+
"status_code": status_code,
|
871
|
+
"contains": _contains,
|
872
|
+
"icontains": _icontains,
|
873
|
+
"startswith": _startswith,
|
874
|
+
"endswith": _endswith,
|
875
|
+
"equals": _equals,
|
876
|
+
"regex": _regex,
|
877
|
+
"header": _header,
|
878
|
+
"count": _count,
|
879
|
+
"status_in": _status_in,
|
880
|
+
"status_between": _status_between,
|
881
|
+
"json_get": _json,
|
882
|
+
"present": _present,
|
883
|
+
"len": len,
|
884
|
+
"any": any,
|
885
|
+
"all": all,
|
886
|
+
"status": lambda: status_code,
|
887
|
+
"lower": lambda s: s.lower() if isinstance(s, str) else s,
|
888
|
+
"upper": lambda s: s.upper() if isinstance(s, str) else s,
|
889
|
+
}
|
890
|
+
|
891
|
+
safe_globals = {"__builtins__": {}}
|
892
|
+
|
893
|
+
for expression in expressions:
|
894
|
+
expr = expression.strip()
|
895
|
+
if not expr:
|
896
|
+
continue
|
897
|
+
try:
|
898
|
+
if not bool(eval(expr, safe_globals, env)):
|
899
|
+
return False
|
900
|
+
except Exception as exc:
|
901
|
+
logger.debug("template.dsl.error", expression=expr, error=str(exc))
|
902
|
+
return False
|
903
|
+
|
904
|
+
return True
|
905
|
+
|
906
|
+
def _get_response_part(self, response: httpx.Response, part: str) -> str:
|
907
|
+
"""Extrai parte específica da response."""
|
908
|
+
if part == "body":
|
909
|
+
return response.text
|
910
|
+
elif part == "header":
|
911
|
+
return str(response.headers)
|
912
|
+
elif part == "all":
|
913
|
+
return f"{response.headers}\n\n{response.text}"
|
914
|
+
else:
|
915
|
+
return response.text
|
916
|
+
|
917
|
+
def _extract_data(self, extractors: List[Dict[str, Any]], response: httpx.Response) -> Dict[str, Any]:
|
918
|
+
"""Extrai dados da response."""
|
919
|
+
extracted = {}
|
920
|
+
|
921
|
+
for extractor in extractors:
|
922
|
+
extractor_type = extractor.get("type", "regex")
|
923
|
+
name = extractor.get("name", "data")
|
924
|
+
part = extractor.get("part", "body")
|
925
|
+
|
926
|
+
content = self._get_response_part(response, part)
|
927
|
+
|
928
|
+
if extractor_type == "regex":
|
929
|
+
regexes = extractor.get("regex", [])
|
930
|
+
for regex in regexes:
|
931
|
+
match = re.search(regex, content, re.IGNORECASE)
|
932
|
+
if match:
|
933
|
+
if match.groups():
|
934
|
+
extracted[name] = match.group(1)
|
935
|
+
else:
|
936
|
+
extracted[name] = match.group(0)
|
937
|
+
|
938
|
+
elif extractor_type == "kval":
|
939
|
+
# Key-value extraction
|
940
|
+
kval = extractor.get("kval", [])
|
941
|
+
# Busca padrão key=value
|
942
|
+
for key in kval:
|
943
|
+
pattern = f"{key}=([^&\\s]+)"
|
944
|
+
match = re.search(pattern, content)
|
945
|
+
if match:
|
946
|
+
extracted[key] = match.group(1)
|
947
|
+
|
948
|
+
elif extractor_type == "json":
|
949
|
+
try:
|
950
|
+
data_json = json.loads(content)
|
951
|
+
except json.JSONDecodeError:
|
952
|
+
continue
|
953
|
+
|
954
|
+
paths = extractor.get("json", [])
|
955
|
+
if isinstance(paths, str):
|
956
|
+
paths = [paths]
|
957
|
+
|
958
|
+
for path_expr in paths:
|
959
|
+
value = self._extract_from_json(data_json, path_expr)
|
960
|
+
if value is not None:
|
961
|
+
extracted[name] = value
|
962
|
+
|
963
|
+
return extracted
|
964
|
+
|
965
|
+
def _extract_from_json(self, data: Any, path_expr: str) -> Optional[Any]:
|
966
|
+
"""Suporte simples para extração JSON via notação ponto."""
|
967
|
+
if not path_expr:
|
968
|
+
return None
|
969
|
+
|
970
|
+
current = data
|
971
|
+
for part in path_expr.split('.'):
|
972
|
+
if part == '':
|
973
|
+
continue
|
974
|
+
if isinstance(current, list):
|
975
|
+
try:
|
976
|
+
index = int(part)
|
977
|
+
current = current[index]
|
978
|
+
except (ValueError, IndexError):
|
979
|
+
return None
|
980
|
+
elif isinstance(current, dict):
|
981
|
+
if part not in current:
|
982
|
+
return None
|
983
|
+
current = current.get(part)
|
984
|
+
else:
|
985
|
+
return None
|
986
|
+
|
987
|
+
return current
|
988
|
+
|
989
|
+
def _create_example_templates(self):
|
990
|
+
"""Cria templates de exemplo."""
|
991
|
+
templates_dir = Path(self.templates_path)
|
992
|
+
templates_dir.mkdir(parents=True, exist_ok=True)
|
993
|
+
|
994
|
+
# Template exemplo: Git exposure
|
995
|
+
git_template = {
|
996
|
+
"id": "git-config",
|
997
|
+
"info": {
|
998
|
+
"name": "Git Config Exposure",
|
999
|
+
"severity": "high",
|
1000
|
+
"description": "Detects exposed .git/config file",
|
1001
|
+
},
|
1002
|
+
"requests": [
|
1003
|
+
{
|
1004
|
+
"method": "GET",
|
1005
|
+
"path": ["/.git/config"],
|
1006
|
+
}
|
1007
|
+
],
|
1008
|
+
"matchers": [
|
1009
|
+
{
|
1010
|
+
"type": "word",
|
1011
|
+
"words": ["[core]", "[remote"],
|
1012
|
+
"condition": "and",
|
1013
|
+
},
|
1014
|
+
{
|
1015
|
+
"type": "status",
|
1016
|
+
"status": [200],
|
1017
|
+
}
|
1018
|
+
],
|
1019
|
+
}
|
1020
|
+
|
1021
|
+
with open(templates_dir / "git-config.yaml", 'w') as f:
|
1022
|
+
yaml.dump(git_template, f)
|
1023
|
+
|
1024
|
+
logger.info("template.examples.created", path=str(templates_dir))
|
1025
|
+
|
1026
|
+
def export(self, findings: List[TemplateFinding], output: str):
|
1027
|
+
"""Exporta findings para arquivo."""
|
1028
|
+
import json
|
1029
|
+
|
1030
|
+
data = [
|
1031
|
+
{
|
1032
|
+
"template_id": f.template_id,
|
1033
|
+
"name": f.name,
|
1034
|
+
"severity": f.severity,
|
1035
|
+
"description": f.description,
|
1036
|
+
"matched_at": f.matched_at,
|
1037
|
+
"extracted_data": f.extracted_data,
|
1038
|
+
}
|
1039
|
+
for f in findings
|
1040
|
+
]
|
1041
|
+
|
1042
|
+
with open(output, 'w') as f:
|
1043
|
+
json.dump(data, f, indent=2)
|
1044
|
+
|
1045
|
+
logger.info("template.export.complete", file=output, count=len(findings))
|
1046
|
+
|
1047
|
+
|
1048
|
+
__all__ = ["TemplateScanner", "Template", "TemplateFinding"]
|