Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 163 additions & 2 deletions src/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Comment thread Fixed
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 #
###########
Expand Down Expand Up @@ -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()
93 changes: 93 additions & 0 deletions src/ghas_cli/utils/dependabot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python3

from typing import List
import requests
import json
import time

Check notice

Code scanning / CodeQL

Unused import

Import of 'time' is not used.
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):
Comment thread
jboursier-mwb marked this conversation as resolved.
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:

Check notice

Code scanning / CodeQL

Except block handles 'BaseException'

Except block directly handles BaseException.
try:
license = dep['licenseDeclared']
except:

Check notice

Code scanning / CodeQL

Except block handles 'BaseException'

Except block directly handles BaseException.
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
20 changes: 20 additions & 0 deletions src/ghas_cli/utils/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down