Last active
April 4, 2022 14:56
-
-
Save scovetta/08311824fbc63b82fe89177817e835a9 to your computer and use it in GitHub Desktop.
This script can be used to automate reporting of malware to the npm team, as a shortcut to npmjs.com/support.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
""" | |
This script will automate reporting malware to the npm team. | |
It automates reporting via https://npmjs.com/support. | |
Before using it, either ensure you have the Requests module installed, or install it with | |
`pip install requests`. | |
To use it, run the script with the following arguments: | |
python npm-report-malware.py | |
-c "Add a comment to the report" | |
-e "[email protected]" | |
-r "Your name" | |
package-name | |
another-package-name@version | |
... | |
If you'd like you can add a `--dry-run` flag to the script to see what it would do without actually | |
sending the report. | |
This script is not officially supported. Your mileage may vary. | |
Author: Michael Scovetta <[email protected]> | |
License: MIT | |
Copyright: Microsoft Corporation | |
Last Updated: 4/3/2022 | |
""" | |
import json | |
import requests | |
import argparse | |
import re | |
import logging | |
logging.basicConfig(level=logging.ERROR) | |
class NpmReportMalware: | |
packages = [] | |
comment = "" | |
""" | |
Initializes the malware reporter. | |
""" | |
def __init__(self): | |
self.logger = logging.getLogger(__name__) | |
self.session = requests.Session() | |
self.csrf_token = self.get_csrf_token() | |
self.logger.debug("csrf_token: %s", self.csrf_token) | |
if not self.csrf_token: | |
raise Exception("Unable to get CSRF token, cannot continue.") | |
""" | |
Processes input and reports the packages. | |
""" | |
def main(self): | |
parser = argparse.ArgumentParser(description="Report malware to npm") | |
parser.add_argument( | |
"--dry-run", help="Do not report, just print what would be sent.", action="store_true" | |
) | |
parser.add_argument( | |
"-c", "--comment", help="Include comments with the report", required=False | |
) | |
parser.add_argument("-r", "--reporter-name", help="Name of the reporter", required=True) | |
parser.add_argument("-e", "--reporter-email", help="Email of the reporter", required=True) | |
parser.add_argument( | |
"package", type=str, nargs="+", help="Package(s) to report (@version optional)" | |
) | |
args = parser.parse_args() | |
self.reporter_name = args.reporter_name | |
self.reporter_email = args.reporter_email | |
for package in args.package: | |
parts = package.split("@") | |
if len(parts) == 2: | |
self.packages.append({"name": parts[0], "version": parts[1]}) | |
else: | |
self.packages.append({"name": package, "version": None}) | |
self.comment = args.comment | |
self.report_malware(args.dry_run) | |
def get_csrf_token(self): | |
try: | |
response = self.session.get("https://www.npmjs.com/support?inquire=security") | |
response.raise_for_status() | |
match = re.search( | |
'<input type="hidden" name="csrftoken" value="([^"]+)"/>', | |
response.text, | |
re.IGNORECASE, | |
) | |
if match: | |
return match.group(1) | |
except: | |
self.logger.error("Failed to get csrf token", exc_info=True) | |
return None | |
def report_malware(self, dry_run=False): | |
payload = { | |
"inquire": "security", | |
"security-inquire": "malware", | |
"name": self.reporter_name, | |
"email": self.reporter_email, | |
"csrftoken": self.csrf_token, | |
} | |
if len(self.packages) == 1: | |
if self.packages[0]["version"]: | |
payload[ | |
"subject" | |
] = f"Malware report in {self.packages[0]['name']}@{self.packages[0]['version']}" | |
else: | |
payload["subject"] = f"Malware report in {self.packages[0]['name']}" | |
payload["package"] = self.packages[0]["name"] | |
payload["version"] = self.packages[0]["version"] or "Unspecified" | |
else: | |
payload["subject"] = "Malware report in {} package(s)".format(len(self.packages)) | |
payload["package"] = "Multiple packages" | |
payload["version"] = "Unspecified" | |
# Create the message / comments | |
_comment = "" | |
if self.comment: | |
_comment = self.comment.replace("\\n", "\n") + "\n\n" | |
_comment += "Packages:\n" | |
for package in self.packages: | |
if package["version"]: | |
_comment += f" - {package['name']}@{package['version']}\n" | |
else: | |
_comment += f" - {package['name']}\n" | |
payload["message"] = _comment | |
if dry_run: | |
print("Payload:") | |
print(json.dumps(payload, indent=2)) | |
else: | |
try: | |
response = self.session.post("https://www.npmjs.com/support", data=payload) | |
response.raise_for_status() | |
print(f"Reported {len(self.packages)} package(s)") | |
self.logger.info("Reported %d package(s)", len(self.packages)) | |
except Exception as msg: | |
print(f"Failed to send report: {msg}") | |
self.logger.error("Failed to send report", exc_info=True) | |
if __name__ == "__main__": | |
reporter = NpmReportMalware() | |
reporter.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment