diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 09ce213..93afdab 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,80 @@ -# StackStorm Exchange Incubator +# Shodan StackStorm Pack -### What is this? +StackStorm integration pack for [Shodan](https://www.shodan.io/) — the search engine for Internet-connected devices. -This repository is a very special place where user-submitted packs get reviewed, perfected, approved, and finally transferred to the Exchange. +## Requirements -If you want to submit your pack, it's simple! +- A Shodan API key — get one at [account.shodan.io](https://account.shodan.io/) -1. **Fork this repo,** -2. **create a subdirectory with your pack,** -3. **and open a Pull Request.** +## Installation -We'll take it from here. Even if your pack is work-in-progress, you can still submit it to get advice and early feedback from our engineers! Or ping us [on Slack](https://stackstorm.com/community-signup), which is generally the best place to get advice from the StackStorm Community. +```shell +st2 pack install shodan +``` -Before you submit a pack, make sure to read the [Create and Contribute a Pack](https://docs.stackstorm.com/reference/packs.html) section of our documentation. +## Configuration -Here's N.E.P.T.R. the StackStorm Exchange Governor, giving you a thumbs-up: +```shell +st2 pack config shodan +``` -![](http://i.imgur.com/3bqVAh0.gif) +Or create the config manually: -## Contributors License Agreement +```yaml +# /opt/stackstorm/configs/shodan.yaml +shodan_api_key: "YOUR_API_KEY_HERE" +request_timeout: 30 +``` -By contributing you agree that these contributions are your own (or approved by your employer) and -you grant a full, complete, irrevocable copyright license to all users and developers of the -project, present and future, pursuant to the license of the project. +Then reload: + +```shell +st2ctl reload --register-configs +``` + +## Actions + +| Action | Description | +|--------|-------------| +| `shodan.host_info` | Returns all Shodan data for a host — ports, banners, hostnames, geolocation, org, CVEs | +| `shodan.host_ports` | Returns open ports only for a host (faster than `host_info`) | +| `shodan.search` | Search Shodan using filter syntax | +| `shodan.dns_resolve` | Resolve one or more hostnames to IP addresses | +| `shodan.dns_reverse` | Reverse DNS lookup for one or more IP addresses | + +## Usage + +```shell +# Full host lookup +st2 run shodan.host_info ip=8.8.8.8 + +# Open ports only +st2 run shodan.host_ports ip=8.8.8.8 + +# Search +st2 run shodan.search query="product:redis country:VN" +st2 run shodan.search query="port:22 has:vuln" page=2 + +# DNS +st2 run shodan.dns_resolve hostnames='["google.com", "cloudflare.com"]' +st2 run shodan.dns_reverse ips='["8.8.8.8", "1.1.1.1"]' +``` + +## Shodan Query Filter Reference + +| Filter | Example | +|--------|---------| +| `port:` | `port:22` | +| `country:` | `country:VN` | +| `org:` | `org:"Amazon"` | +| `product:` | `product:nginx` | +| `os:` | `os:windows` | +| `asn:` | `asn:AS13335` | +| `vuln:` | `vuln:CVE-2021-44228` | +| `has:vuln` | `has:vuln` | + +Full filter reference: https://www.shodan.io/search/filters ## License -All packs must be licensed under the Apache 2.0 license. We will not accept your new pack -until we see a LICENSE file in your pull request with the Apache 2.0 license in it. +Apache 2.0 \ No newline at end of file diff --git a/actions/dns_resolve.py b/actions/dns_resolve.py new file mode 100644 index 0000000..c7db923 --- /dev/null +++ b/actions/dns_resolve.py @@ -0,0 +1,14 @@ +from lib.base import ShodanBaseAction + +class DnsResolveAction(ShodanBaseAction): + def run(self, hostnames): + try: + if isinstance(hostnames, list): + hostnames_str = ",".join(hostnames) + else: + hostnames_str = hostnames + args = {"hostnames": hostnames_str} + result = self.client._request('/dns/resolve', args) + return True, result + except Exception as e: + return False, str(e) \ No newline at end of file diff --git a/actions/dns_resolve.yaml b/actions/dns_resolve.yaml new file mode 100644 index 0000000..608c2fc --- /dev/null +++ b/actions/dns_resolve.yaml @@ -0,0 +1,16 @@ +--- +name: dns_resolve +pack: shodan +runner_type: python-script +description: > + Look up the IP addresses for one or more hostnames. Uses Shodan's DNS + resolution endpoint. +enabled: true +entry_point: dns_resolve.py +parameters: + hostnames: + type: array + description: List of hostnames to resolve (e.g. ["google.com", "example.com"]). + required: true + items: + type: string \ No newline at end of file diff --git a/actions/dns_reverse.py b/actions/dns_reverse.py new file mode 100644 index 0000000..8bf2988 --- /dev/null +++ b/actions/dns_reverse.py @@ -0,0 +1,15 @@ +from lib.base import ShodanBaseAction + +class DnsReverseAction(ShodanBaseAction): + def run(self, ips): + try: + if isinstance(ips, list): + ips_str = ",".join(ips) + else: + ips_str = ips + args = {"ips": ips_str} + result = self.client._request('/dns/reverse', args) + + return True, result + except Exception as e: + return False, str(e) \ No newline at end of file diff --git a/actions/dns_reverse.yaml b/actions/dns_reverse.yaml new file mode 100644 index 0000000..9b87c25 --- /dev/null +++ b/actions/dns_reverse.yaml @@ -0,0 +1,16 @@ +--- +name: dns_reverse +pack: shodan +runner_type: python-script +description: > + Perform reverse DNS lookups on one or more IP addresses. Returns the + hostnames that point to each IP. +enabled: true +entry_point: dns_reverse.py +parameters: + ips: + type: array + description: List of IPv4 addresses to reverse-lookup (e.g. ["8.8.8.8", "1.1.1.1"]). + required: true + items: + type: string \ No newline at end of file diff --git a/actions/host_info.py b/actions/host_info.py new file mode 100644 index 0000000..a456c87 --- /dev/null +++ b/actions/host_info.py @@ -0,0 +1,12 @@ +"""Return all available Shodan data for a given IP address.""" + +from lib.base import ShodanBaseAction + + +class HostInfoAction(ShodanBaseAction): + def run(self, ip, history=False, minify=False): + try: + result = self.client.host(ip, history=history, minify=minify) + return True, result + except Exception as e: + return False, str(e) diff --git a/actions/host_info.yaml b/actions/host_info.yaml new file mode 100644 index 0000000..bd8ca8c --- /dev/null +++ b/actions/host_info.yaml @@ -0,0 +1,24 @@ +--- +name: host_info +pack: shodan +runner_type: python-script +description: > + Returns all services that have been found on the given host IP. Includes open + ports, banners, vulnerabilities (if any), geolocation, hostnames, and more. +enabled: true +entry_point: host_info.py +parameters: + ip: + type: string + description: IPv4 address of the host to query. + required: true + history: + type: boolean + description: Include all historical banners for the host. + required: false + default: false + minify: + type: boolean + description: Return only the minimal set of host information (faster). + required: false + default: false \ No newline at end of file diff --git a/actions/host_ports.py b/actions/host_ports.py new file mode 100644 index 0000000..b729d28 --- /dev/null +++ b/actions/host_ports.py @@ -0,0 +1,10 @@ +from lib.base import ShodanBaseAction + +class HostPortsAction(ShodanBaseAction): + def run(self, ip): + try: + result = self.client.host(ip, minify=True) + ports = result.get("ports", []) + return True, {"ip": ip, "ports": sorted(ports)} + except Exception as e: + return False, str(e) \ No newline at end of file diff --git a/actions/host_ports.yaml b/actions/host_ports.yaml new file mode 100644 index 0000000..9639512 --- /dev/null +++ b/actions/host_ports.yaml @@ -0,0 +1,14 @@ +--- +name: host_ports +pack: shodan +runner_type: python-script +description: > + Returns the list of open ports Shodan has observed on a host. Faster than + host_info as it fetches only minimal data. +enabled: true +entry_point: host_ports.py +parameters: + ip: + type: string + description: IPv4 address of the host. + required: true \ No newline at end of file diff --git a/actions/lib/base.py b/actions/lib/base.py new file mode 100644 index 0000000..bb2ac12 --- /dev/null +++ b/actions/lib/base.py @@ -0,0 +1,16 @@ +import shodan +from st2common.runners.base_action import Action + + +class ShodanBaseAction(Action): + def __init__(self, config): + super().__init__(config) + api_key = self.config.get("shodan_api_key") + if not api_key: + raise ValueError( + "shodan_api_key is not configured. " + "Please set it via: st2 pack config shodan" + ) + timeout = self.config.get("request_timeout", 30) + self.client = shodan.Shodan(api_key) + self.client.timeout = timeout \ No newline at end of file diff --git a/actions/search.py b/actions/search.py new file mode 100644 index 0000000..20323e8 --- /dev/null +++ b/actions/search.py @@ -0,0 +1,43 @@ +from lib.base import ShodanBaseAction + +MATCH_FIELDS = ( + "ip_str", "port", "transport", "hostnames", "domains", + "org", "isp", "product", "version", "os", "cpe", "cpe23", + "asn", "vulns", "tags", "timestamp", "location", +) + +class SearchAction(ShodanBaseAction): + def run(self, query, facets=None, page=1, minify=True): + try: + kwargs = {"page": page, "minify": minify} + if facets: + kwargs["facets"] = facets + + result = self.client.search(query, **kwargs) + matches = [self._trim(m) for m in result.get("matches", [])] + + return True, { + "total": result.get("total", 0), + "page": page, + "count": len(matches), + "matches": matches, + "facets": result.get("facets", {}), + } + except Exception as e: + return False, str(e) + + def _trim(self, match): + trimmed = {k: match[k] for k in MATCH_FIELDS if k in match} + + loc = trimmed.pop("location", {}) or {} + trimmed["country_code"] = loc.get("country_code", "") + trimmed["country_name"] = loc.get("country_name", "") + trimmed["city"] = loc.get("city", "") + + vulns = trimmed.get("vulns") + if isinstance(vulns, dict): + trimmed["vulns"] = { + cve: data.get("summary", "") for cve, data in vulns.items() + } + + return trimmed \ No newline at end of file diff --git a/actions/search.yaml b/actions/search.yaml new file mode 100644 index 0000000..3597f56 --- /dev/null +++ b/actions/search.yaml @@ -0,0 +1,33 @@ +--- +name: search +pack: shodan +runner_type: python-script +description: > + Search Shodan using the same query syntax as the website. Returns a list of + matching hosts with their banners, open ports, and metadata. +enabled: true +entry_point: search.py +parameters: + query: + type: string + description: > + Shodan search query (e.g. "apache country:US port:8080", + "product:nginx vuln:CVE-2021-44228"). + required: true + facets: + type: string + description: > + Comma-separated list of properties to get summary info on, e.g. + "country,org,port". Requires a paid API plan. + required: false + default: null + page: + type: integer + description: Page number of results (100 results per page). Starts at 1. + required: false + default: 1 + minify: + type: boolean + description: Return minimal host information per result to reduce payload size. + required: false + default: true \ No newline at end of file diff --git a/config.schema.yaml b/config.schema.yaml new file mode 100644 index 0000000..9252783 --- /dev/null +++ b/config.schema.yaml @@ -0,0 +1,5 @@ +shodan_api_key: + description: Shodan API key. Get one at https://account.shodan.io/ + type: string + secret: true + required: true \ No newline at end of file diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..b39eaba Binary files /dev/null and b/icon.png differ diff --git a/pack.yaml b/pack.yaml new file mode 100644 index 0000000..81f6cc8 --- /dev/null +++ b/pack.yaml @@ -0,0 +1,17 @@ +ref: shodan +name: shodan +description: StackStorm integration pack for Shodan - the search engine for Internet-connected devices. +keywords: + - shodan + - security + - threat intelligence + - network + - scanning +version: 1.0.0 +python_versions: + - "3" +author: Shodan +email: support@shodan.io +contributors: + - "Duong Dinh C " + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..859a8a2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +shodan \ No newline at end of file