Skip to content

Commit

Permalink
Reset everything
Browse files Browse the repository at this point in the history
  • Loading branch information
seanh committed Oct 25, 2023
1 parent 0a12996 commit f1bb5e2
Show file tree
Hide file tree
Showing 6 changed files with 19 additions and 230 deletions.
1 change: 0 additions & 1 deletion .cookiecutter/includes/setuptools/install_requires

This file was deleted.

1 change: 0 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package_dir =
packages = find:
python_requires = >=3.11
install_requires =
requests

[options.packages.find]
where = src
Expand Down
46 changes: 5 additions & 41 deletions src/dependabot_alerts/cli.py
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
186 changes: 2 additions & 184 deletions src/dependabot_alerts/core.py
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!"
9 changes: 6 additions & 3 deletions tests/unit/dependabot_alerts/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from dependabot_alerts.cli import cli


def test_it():
cli([])


def test_help():
with pytest.raises(SystemExit) as exc_info:
cli(["--help"])
Expand All @@ -13,8 +17,7 @@ def test_help():


def test_version(capsys):
with pytest.raises(SystemExit) as exc_info:
cli(["--version"])
exit_code = cli(["--version"])

assert capsys.readouterr().out.strip() == version("dependabot-alerts")
assert not exc_info.value.code
assert not exit_code
6 changes: 6 additions & 0 deletions tests/unit/dependabot_alerts/core_test.py
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!"

0 comments on commit f1bb5e2

Please sign in to comment.