-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
19 additions
and
230 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,13 @@ | ||
from argparse import ArgumentParser | ||
from importlib.metadata import version | ||
|
||
from dependabot_alerts.core import GitHubClient, Vulnerability, fetch_alerts | ||
|
||
|
||
def cli(_argv=None): # pragma: no cover | ||
def cli(_argv=None): # pylint:disable=inconsistent-return-statements | ||
parser = ArgumentParser() | ||
parser.add_argument( | ||
"-v", "--version", action="version", version=version("dependabot-alerts") | ||
) | ||
parser.add_argument("organization", help="GitHub user or organization") | ||
parser.add_argument("-v", "--version", action="store_true") | ||
|
||
args = parser.parse_args(_argv) | ||
|
||
gh_client = GitHubClient.init() | ||
vulns = fetch_alerts(gh_client, args.organization) | ||
print(format_slack_message(args.organization, vulns)) | ||
|
||
return 0 | ||
|
||
|
||
def format_slack_message( | ||
organization: str, vulns: list[Vulnerability] | ||
) -> str: # pragma: no cover | ||
""" | ||
Format a Slack status report from a list of vulnerabilities. | ||
Returns a message using Slack's "mrkdwn" format. See | ||
https://api.slack.com/reference/surfaces/formatting. | ||
""" | ||
if not vulns: | ||
return "Found no open vulnerabilities." | ||
|
||
n_repos = len(set(vuln.repo for vuln in vulns)) | ||
|
||
msg_parts = [] | ||
msg_parts.append(f"*Found {len(vulns)} vulnerabilities in {n_repos} repositories.*") | ||
|
||
for vuln in vulns: | ||
vuln_msg = [] | ||
vuln_msg.append( | ||
f"{organization}/{vuln.repo}: <{vuln.url}|{vuln.package_name} {vuln.severity} - {vuln.title}>" | ||
) | ||
if vuln.pr: | ||
vuln_msg.append(f" Resolved by {vuln.pr}") | ||
msg_parts.append("\n".join(vuln_msg)) | ||
|
||
return "\n\n".join(msg_parts) | ||
if args.version: | ||
print(version("dependabot-alerts")) | ||
return 0 |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,184 +1,2 @@ | ||
import json | ||
import os | ||
from dataclasses import dataclass | ||
from getpass import getpass | ||
from typing import Optional | ||
|
||
import requests | ||
|
||
|
||
class GitHubClient: # pragma: no cover | ||
""" | ||
Client for GitHub's GraphQL API. | ||
See https://docs.github.com/en/graphql. | ||
""" | ||
|
||
def __init__(self, token): | ||
self.token = token | ||
self.endpoint = "https://api.github.com/graphql" | ||
|
||
def query(self, query, variables=None): | ||
data = {"query": query, "variables": variables if variables is not None else {}} | ||
result = requests.post( | ||
url=self.endpoint, | ||
headers={"Authorization": f"Bearer {self.token}"}, | ||
data=json.dumps(data), | ||
timeout=(30, 30), | ||
) | ||
body = result.json() | ||
result.raise_for_status() | ||
if "errors" in body: | ||
errors = body["errors"] | ||
raise RuntimeError(f"Query failed: {json.dumps(errors)}") | ||
return body["data"] | ||
|
||
@classmethod | ||
def init(cls): | ||
""" | ||
Initialize an authenticated GitHubClient. | ||
This will read from the `GITHUB_TOKEN` env var if set, or prompt for | ||
a token otherwise. | ||
""" | ||
access_token = os.environ.get("GITHUB_TOKEN") | ||
if not access_token: | ||
access_token = getpass("GitHub API token: ") | ||
return GitHubClient(access_token) | ||
|
||
|
||
@dataclass | ||
class Vulnerability: # pylint:disable=too-many-instance-attributes | ||
repo: str | ||
"""Repository where this vulnerability was reported.""" | ||
|
||
created_at: str | ||
"""ISO date when this alert was created.""" | ||
|
||
package_name: str | ||
"""Name of the vulnerable package.""" | ||
|
||
ecosystem: str | ||
"""Package ecosytem (eg. npm) that the package comes from.""" | ||
|
||
severity: str | ||
"""Vulnerability severity level.""" | ||
|
||
version_range: str | ||
"""Version ranges of package affected by vulnerability.""" | ||
|
||
number: str | ||
"""Number of this vulnerability report.""" | ||
|
||
url: str | ||
"""Link to the vulernability report on GitHub.""" | ||
|
||
pr: Optional[str] | ||
"""Link to the Dependabot update PR that resolves this vulnerability.""" | ||
|
||
title: str | ||
"""Summary of what the vulnerability is.""" | ||
|
||
|
||
def fetch_alerts( | ||
gh: GitHubClient, organization: str | ||
) -> list[Vulnerability]: # pragma: no cover | ||
""" | ||
Fetch details of all open vulnerability alerts in `organization`. | ||
To reduce the volume of noise, especially for repositories which include the | ||
same dependency in multiple lockfiles, only one vulnerability is reported | ||
per package per repository. | ||
Vulnerabilities are not reported from archived repositories. | ||
""" | ||
# pylint:disable=too-many-locals | ||
|
||
query = """ | ||
query($organization: String!, $cursor: String) { | ||
organization(login: $organization) { | ||
repositories(first: 100, after: $cursor) { | ||
pageInfo { | ||
endCursor | ||
hasNextPage | ||
} | ||
nodes { | ||
name | ||
vulnerabilityAlerts(first: 100, states:OPEN) { | ||
nodes { | ||
number | ||
createdAt | ||
dependabotUpdate { | ||
pullRequest { | ||
url | ||
} | ||
} | ||
securityAdvisory { | ||
summary | ||
} | ||
securityVulnerability { | ||
package { | ||
name | ||
ecosystem | ||
} | ||
severity | ||
vulnerableVersionRange | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
""" | ||
|
||
vulns = [] | ||
cursor = None | ||
has_next_page = True | ||
|
||
while has_next_page: | ||
result = gh.query( | ||
query=query, variables={"organization": organization, "cursor": cursor} | ||
) | ||
page_info = result["organization"]["repositories"]["pageInfo"] | ||
cursor = page_info["endCursor"] | ||
has_next_page = page_info["hasNextPage"] | ||
|
||
for repo in result["organization"]["repositories"]["nodes"]: | ||
alerts = repo["vulnerabilityAlerts"]["nodes"] | ||
|
||
if alerts: | ||
repo_name = repo["name"] | ||
vulnerable_packages = set() | ||
|
||
for alert in alerts: | ||
sa = alert["securityAdvisory"] | ||
sv = alert["securityVulnerability"] | ||
number = alert["number"] | ||
package_name = sv["package"]["name"] | ||
|
||
if package_name in vulnerable_packages: | ||
continue | ||
vulnerable_packages.add(package_name) | ||
|
||
pr = None | ||
|
||
dep_update = alert["dependabotUpdate"] | ||
if dep_update and dep_update["pullRequest"]: | ||
pr = dep_update["pullRequest"]["url"] | ||
|
||
vuln = Vulnerability( | ||
repo=repo_name, | ||
created_at=alert["createdAt"], | ||
ecosystem=sv["package"]["ecosystem"], | ||
number=number, | ||
package_name=sv["package"]["name"], | ||
pr=pr, | ||
severity=sv["severity"], | ||
title=sa["summary"], | ||
url=f"https://github.com/{organization}/{repo_name}/security/dependabot/{number}", | ||
version_range=sv["vulnerableVersionRange"], | ||
) | ||
vulns.append(vuln) | ||
|
||
return vulns | ||
def hello_world(): | ||
return "Hello, world!" |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from dependabot_alerts.core import hello_world | ||
|
||
|
||
class TestHelloWorld: | ||
def test_it(self): | ||
assert hello_world() == "Hello, world!" |