From bb0856497067d030216f70138a7a55afa2c25621 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:29:45 +0000 Subject: [PATCH 1/3] Initial plan From d10d955fced4b638b6368010ea44356a68e6011a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:37:15 +0000 Subject: [PATCH 2/3] Add jsleak task and integrate it into url_crawl workflow Co-authored-by: ocervell <9629314+ocervell@users.noreply.github.com> --- secator/configs/workflows/url_crawl.yaml | 18 ++++- secator/tasks/jsleak.py | 98 ++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 secator/tasks/jsleak.py diff --git a/secator/configs/workflows/url_crawl.yaml b/secator/configs/workflows/url_crawl.yaml index 0c3b1c8d4..7bc7dd82b 100644 --- a/secator/configs/workflows/url_crawl.yaml +++ b/secator/configs/workflows/url_crawl.yaml @@ -16,7 +16,7 @@ options: crawlers: type: list help: "Crawlers to use (comma-separated) (passive: xurlfind3r, urlfinder, gau; active: katana, gospider, cariddi)" - default: ['xurlfind3r', 'katana'] + default: ['xurlfind3r', 'katana', 'gospider'] internal: True hunt_secrets: @@ -25,6 +25,12 @@ options: default: False short: hs + hunt_js_secrets: + is_flag: True + help: Hunt secrets in JavaScript files (jsleak) + default: False + short: hjs + default_options: match_codes: 200,204,301,302,307,401,403,405,500 @@ -76,3 +82,13 @@ tasks: field: stored_response_path condition: item.stored_response_path != '' if: opts.hunt_secrets and not opts.passive + + jsleak: + description: Hunt secrets in JavaScript files + secrets: True + status_check: True + targets_: + - type: url + field: url + condition: "item.url.endswith('.js') or 'javascript' in item.content_type.lower()" + if: opts.hunt_js_secrets and not opts.passive diff --git a/secator/tasks/jsleak.py b/secator/tasks/jsleak.py new file mode 100644 index 000000000..78d1cd145 --- /dev/null +++ b/secator/tasks/jsleak.py @@ -0,0 +1,98 @@ +from secator.decorators import task +from secator.definitions import OPT_NOT_SUPPORTED, OPT_PIPE_INPUT, URL +from secator.output_types import Tag, Url +from secator.serializers import JSONSerializer +from secator.tasks._categories import HttpCrawler + + +@task() +class jsleak(HttpCrawler): + """Find secrets and links in JavaScript files.""" + cmd = 'jsleak' + input_types = [URL] + output_types = [Tag, Url] + tags = ['js', 'secret', 'scan'] + input_flag = OPT_PIPE_INPUT + file_flag = OPT_PIPE_INPUT + json_flag = '-j' + opts = { + 'secrets': {'is_flag': True, 'short': 's', 'default': True, 'help': 'Search for secrets'}, + 'links': {'is_flag': True, 'short': 'l', 'default': False, 'help': 'Find links/endpoints'}, + 'complete': {'is_flag': True, 'short': 'e', 'default': False, 'help': 'Extract complete URLs'}, + 'status_check': {'is_flag': True, 'short': 'k', 'default': True, 'help': 'Check status codes of found URLs'}, + 'concurrency': {'type': int, 'short': 'c', 'default': 20, 'help': 'Number of concurrent requests'}, + 'pattern_file': {'type': str, 'short': 't', 'help': 'Path to custom regex pattern YAML file'}, + } + opt_key_map = { + 'secrets': 's', + 'links': 'l', + 'complete': 'e', + 'status_check': 'k', + 'concurrency': 'c', + 'pattern_file': 't', + } + item_loaders = [JSONSerializer()] + install_version = 'v1.1.0' + install_cmd = 'go install -v github.com/channyein1337/jsleak@[install_version]' + github_handle = 'channyein1337/jsleak' + proxychains = False + proxy_socks5 = False + proxy_http = False + profile = 'io' + + @staticmethod + def on_json_loaded(self, item): + """Process JSON output from jsleak.""" + # jsleak returns different types of data + item_type = item.get('type', '') + url = item.get('url', '') + + if item_type == 'secret': + # Secret found + name = item.get('name', 'unknown_secret') + match_value = item.get('match', '') + extra_data = { + 'content': match_value, + 'url': url, + } + + # Add any additional fields from jsleak + for key in ['confidence', 'line', 'pattern']: + if key in item: + extra_data[key] = item[key] + + yield Tag( + category='secret', + name=name.lower().replace(' ', '_').replace('-', '_'), + match=url, + extra_data=extra_data + ) + + elif item_type == 'link' or item_type == 'endpoint': + # URL/endpoint found + found_url = item.get('link', item.get('endpoint', '')) + if found_url: + status_code = item.get('status', 0) + yield Url( + url=found_url, + status_code=status_code, + extra_data={'source_url': url} + ) + + elif 'secret' in item or 'match' in item: + # Fallback for simple secret format + match_value = item.get('match', item.get('secret', '')) + name = item.get('name', item.get('type', 'leaked_secret')) + + yield Tag( + category='secret', + name=name.lower().replace(' ', '_').replace('-', '_'), + match=url or 'unknown', + extra_data={'content': match_value, 'url': url} + ) + + elif 'url' in item or 'link' in item: + # Fallback for simple URL format + found_url = item.get('url', item.get('link', '')) + if found_url: + yield Url(url=found_url) From 142bd917d43002f59cc556056dcc00bed08e9b77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:38:55 +0000 Subject: [PATCH 3/3] Simplify jsleak on_json_loaded to match actual tool output format Co-authored-by: ocervell <9629314+ocervell@users.noreply.github.com> --- secator/tasks/jsleak.py | 68 +++++++++++++---------------------------- 1 file changed, 22 insertions(+), 46 deletions(-) diff --git a/secator/tasks/jsleak.py b/secator/tasks/jsleak.py index 78d1cd145..dfb675239 100644 --- a/secator/tasks/jsleak.py +++ b/secator/tasks/jsleak.py @@ -42,57 +42,33 @@ class jsleak(HttpCrawler): @staticmethod def on_json_loaded(self, item): - """Process JSON output from jsleak.""" - # jsleak returns different types of data - item_type = item.get('type', '') + """Process JSON output from jsleak. + + jsleak outputs JSON in format: + { + "url": "http://example.com/script.js", + "pattern": "api_key_regex", + "matches": ["secret1", "secret2"] + } + """ url = item.get('url', '') + pattern = item.get('pattern', 'leaked_secret') + matches = item.get('matches', []) - if item_type == 'secret': - # Secret found - name = item.get('name', 'unknown_secret') - match_value = item.get('match', '') - extra_data = { - 'content': match_value, - 'url': url, - } + # Create a Tag for each match found + for match in matches: + if not match: + continue - # Add any additional fields from jsleak - for key in ['confidence', 'line', 'pattern']: - if key in item: - extra_data[key] = item[key] + # Clean up pattern name to use as tag name + name = pattern.lower().replace(' ', '_').replace('-', '_') yield Tag( category='secret', - name=name.lower().replace(' ', '_').replace('-', '_'), + name=name, match=url, - extra_data=extra_data - ) - - elif item_type == 'link' or item_type == 'endpoint': - # URL/endpoint found - found_url = item.get('link', item.get('endpoint', '')) - if found_url: - status_code = item.get('status', 0) - yield Url( - url=found_url, - status_code=status_code, - extra_data={'source_url': url} - ) - - elif 'secret' in item or 'match' in item: - # Fallback for simple secret format - match_value = item.get('match', item.get('secret', '')) - name = item.get('name', item.get('type', 'leaked_secret')) - - yield Tag( - category='secret', - name=name.lower().replace(' ', '_').replace('-', '_'), - match=url or 'unknown', - extra_data={'content': match_value, 'url': url} + extra_data={ + 'content': match, + 'pattern': pattern + } ) - - elif 'url' in item or 'link' in item: - # Fallback for simple URL format - found_url = item.get('url', item.get('link', '')) - if found_url: - yield Url(url=found_url)