diff --git a/src/cli.py b/src/cli.py index b7c0020..a51409e 100644 --- a/src/cli.py +++ b/src/cli.py @@ -22,7 +22,7 @@ logging.error("Missing dependencies. Please reach @jboursier if needed.") sys.exit(255) -from ghas_cli.utils import repositories, vulns, teams, issues, actions, roles, secrets +from ghas_cli.utils import repositories, vulns, teams, issues, actions, roles, secrets, dependabot def main() -> None: @@ -223,6 +223,31 @@ def repositories_list( output.write(r.name + "\n") click.echo(r.name) +@repositories_cli.command("get_topics") +@click.option( + "-r", + "--repository", + prompt="Repository name", +) +@click.option( + "-t", + "--token", + prompt=False, + type=str, + default=None, + hide_input=True, + confirmation_prompt=False, + show_envvar=True, +) +@click.option("-o", "--organization", prompt="Organization name", type=str) +def repositories_get_topics( + repository: str, + organization: str, + token: str, +) -> None: + """Get a repository topics""" + click.echo(repositories.get_topics(token=token, organization=organization, repository_name=repository)) + @repositories_cli.command("enable_ss_protection") @click.option( @@ -830,12 +855,82 @@ def secret_alerts_export( ############## -@cli.group() +@cli.group(name="dependabot") def dependabot_alerts() -> None: """Manage Dependabot alerts""" pass +@dependabot_alerts.command("get_alerts") +@click.option( + "-r", + "--repos", + prompt="Repositories name. Use `all` to retrieve alerts for all repos.", + type=str, + multiple=True, +) +@click.option( + "-t", + "--token", + prompt=False, + type=str, + default=None, + hide_input=True, + confirmation_prompt=False, + show_envvar=True, +) +@click.option("-o", "--organization", prompt="Organization name", type=str) +def dependabot_alerts_list( + repos: List, + organization: str, + token: str, +) -> None: + """Get Dependabot alerts for a repository""" + + for repo in repos: + dependabot_res = dependabot.list_alerts_repo( + repository=repo, + organization=organization, + token=token, + ) + + for res in dependabot_res: + click.echo(res) + + +@dependabot_alerts.command("get_dependencies") +@click.option( + "-f", + "--format", + prompt="Output format", + type=click.Choice( + ["sbom", "csv", "txt"], + case_sensitive=True, + ), + default="sbom", +) +@click.option( + "-r", + "--repository", + prompt="Repository name", +) +@click.option( + "-t", + "--token", + prompt=False, + type=str, + default=None, + hide_input=True, + confirmation_prompt=False, + show_envvar=True, +) +@click.option("-o", "--organization", prompt="Organization name", type=str) +def dependabot_get_dependencies(repository:str, organization:str, token:str, format: str="sbom") -> None: + """Get a list of dependencies for a repository""" + + res = dependabot.get_dependencies(repository, organization, token, format=format) + click.echo(res, nl=False) + ########### # Actions # ########### @@ -1406,5 +1501,71 @@ def mass_set_developer_role( return None + +@mass_cli.command("topics") +@click.argument("input_repos_list", type=click.File("r")) +@click.option( + "-t", + "--token", + prompt=False, + type=str, + default=None, + hide_input=True, + confirmation_prompt=False, + show_envvar=True, +) +@click.option("-o", "--organization", prompt="Organization name", type=str) +def mass_get_topics( + input_repos_list: Any, + organization: str, + token: str, +) -> None: + repos_list = input_repos_list.readlines() + + for repo in repos_list: + repo = repo.rstrip("\n") + + click.echo(f"{repo},", nl=False) + click.echo(repositories.get_topics(token=token, organization=organization, repository_name=repo)) + +@mass_cli.command("dependencies") +@click.argument("input_repos_list", type=click.File("r")) +@click.option( + "-t", + "--token", + prompt=False, + type=str, + default=None, + hide_input=True, + confirmation_prompt=False, + show_envvar=True, +) +@click.option("-o", "--organization", prompt="Organization name", type=str) +@click.option( + "-f", + "--format", + prompt="Output format", + type=click.Choice( + ["sbom", "csv", "txt"], + case_sensitive=True, + ), + default="csv", +) +def mass_get_dependencies( + format: str, + input_repos_list: Any, + organization: str, + token: str, +) -> None: + repos_list = input_repos_list.readlines() + + for repo in repos_list: + repo = repo.rstrip("\n") + + click.echo(f"{repo},", nl=False) + click.echo(dependabot.get_dependencies(repository=repo, organization=organization, token=token, format=format), nl=False) + + + if __name__ == "__main__": main() diff --git a/src/ghas_cli/utils/dependabot.py b/src/ghas_cli/utils/dependabot.py new file mode 100644 index 0000000..ad9d80c --- /dev/null +++ b/src/ghas_cli/utils/dependabot.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +#!/usr/bin/env python3 + +from typing import List +import requests +import json +import time +import logging + +from . import network + + +def list_alerts_repo(repository: str, organization: str, token: str) -> List: + """Get Dependabot alerts for one repository""" + + headers = network.get_github_headers(token) + + alerts_repo = [] + page = 1 + while True: + i = 0 + while i < network.RETRIES: + params = {"state": "open", "per_page": 100, "page": page} + alerts = requests.get( + url=f"https://api.github.com/repos/{organization}/{repository}/dependabot/alerts", + params=params, + headers=headers, + ) + if network.check_rate_limit(alerts): + i += 1 + else: + break + + if alerts.status_code != 200: + break + if not alerts.json(): + break + for a in alerts.json(): + if not a: + continue + alerts_repo.append(json.dumps(a)) + page += 1 + + return alerts_repo + + + +def get_dependencies(repository: str, organization: str, token: str, format:str ="sbom"): + """ + Get the list of dependencies for one repository. + + Available formats: + - `sbom` - SPDX json + - `CSV` - CSV export + - `txt` - basic export + + https://docs.github.com/en/rest/dependency-graph/sboms?apiVersion=2022-11-28 + """ + headers = network.get_github_headers(token) + + dependencies = requests.get( + url=f"https://api.github.com/repos/{organization}/{repository}/dependency-graph/sbom", + headers=headers, + ) + + + if dependencies.status_code != 200: + logging.error(f"Unable to retrieve the dependencies for {repository} - {dependencies.status_code} - {dependencies.content}") + return False + + if "sbom" == format: + return dependencies.json() + elif "csv" == format: + deps = "" + for dep in dependencies.json()["sbom"]["packages"]: + try: + license = dep['licenseConcluded'] + except: + try: + license = dep['licenseDeclared'] + except: + license = "Unknown" + + deps += f"{repository}, {dep['name']},{dep['versionInfo']}, {license}\n" + return deps + elif "txt" == format: + deps = "" + for dep in dependencies.json()["sbom"]["packages"]: + deps += dep["name"] + "\n" + return deps + else: + logging.error(f"Invalid export format {format}. Must be one of `sbom`, `csv` or `txt`.") + return False diff --git a/src/ghas_cli/utils/repositories.py b/src/ghas_cli/utils/repositories.py index 6830f5f..7341a11 100644 --- a/src/ghas_cli/utils/repositories.py +++ b/src/ghas_cli/utils/repositories.py @@ -236,6 +236,26 @@ def get_default_branch_last_updated( branch_res["commit"]["commit"]["author"]["date"].split("T")[0], "%Y-%m-%d" ) +def get_topics( + token: str, organization: str, repository_name: str +) -> List: + """ + Return the repository topics + """ + headers = network.get_github_headers(token) + + topic_res = network.get( + url=f"https://api.github.com/repos/{organization}/{repository_name}/topics", + headers=headers, + ) + + if topic_res.status_code != 200: + return False + + topics_res = topic_res.json() + + return topics_res['names'] + def archive( organization: str, token: str, repository: str, archive: bool = True