diff --git a/src/cli.py b/src/cli.py index 0fa738c..a5c338a 100644 --- a/src/cli.py +++ b/src/cli.py @@ -371,6 +371,115 @@ def repositories_create_dep_enforcement_pr( ) +@repositories_cli.command("archivable") +@click.option( + "-f", + "--format", + prompt="Output format", + type=click.Choice( + ["human", "list"], + case_sensitive=False, + ), + default="list", +) +@click.option( + "-u", "--last_updated_before", prompt="Last updated before YYYY-MM-DD", type=str +) +@click.argument("input_repos_list", type=click.File("r")) +@click.argument("output", type=click.File("w")) +@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_archivable( + last_updated_before: str, + format: str, + input_repos_list: Any, + output: Any, + organization: str, + token: str, +) -> bool: + """Find potentially archivable repositories""" + + try: + threshold_date = datetime.strptime(last_updated_before, "%Y-%m-%d") + except Exception: + click.echo(f"Invalid time: {last_updated_before}") + return False + + # 1. Get list repositories passed as argument + res = input_repos_list.readlines() + + print(len(res)) + for repo in res: + + repo = repo.rstrip("\n") + + # 2. get default branch + default_branch = repositories.get_default_branch( + organization=organization, token=token, repository=repo + ) + if not default_branch: + continue + + # 3. get default branch last commit date + branch_last_commit_date = repositories.get_default_branch_last_updated( + token=token, + organization=organization, + repository_name=repo, + default_branch=default_branch, + ) + if not branch_last_commit_date: + click.echo(f"No branch last commit date for {repo}", err=True) + continue + + # 4. Compare with the threshold + if branch_last_commit_date > threshold_date: + continue + + if "human" == format: + output.write(repo + "\n") + click.echo(repo) + elif "list" == format: + output.write(f"{repo}, {branch_last_commit_date.strftime('%Y-%m-%d')}\n") + click.echo(f"{repo}, {branch_last_commit_date.strftime('%Y-%m-%d')}") + + return True + + +@repositories_cli.command("archive") +@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_archive( + repository: str, + organization: str, + token: str, +) -> None: + """Archive a repository""" + click.echo(repositories.archive(organization, token, repository)) + + ######### # Teams # ######### @@ -882,5 +991,98 @@ def mass_deploy( ) +@mass_cli.command("archive") +@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_issue_archive( + input_repos_list: Any, + organization: str, + token: str, +) -> None: + """Create an issue to inform that repositories will be archived at a specific date.""" + + repos_list = input_repos_list.readlines() + + for repo in repos_list: + + repo = repo.rstrip("\n") + + click.echo(f"{repo}...", nl=False) + + if repositories.archive( + organization=organization, token=token, repository=repo + ): + click.echo(" Archived.") + else: + click.echo(" Not Archived.", err=True) + + +@mass_cli.command("issue_upcoming_archive") +@click.argument("input_repos_list", type=click.File("r")) +@click.option( + "-u", + "--archived_date", + prompt="Target date when the repositories will be archived", + type=str, +) +@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_archive( + input_repos_list: Any, + archived_date: str, + organization: str, + token: str, +) -> None: + """Mass archive a list of repositories""" + + repos_list = input_repos_list.readlines() + + for repo in repos_list: + + repo = repo.rstrip("\n") + + issue_res = issues.create( + title=f"This repository will be archived on {archived_date} :warning: :wastebasket:", + content=f""" +Hello, + +Due to inactivity, this repository will be archived automatically on {archived_date}. + +This means that it will become read-only: `git clone` will still work, and the repository can be unarchived at anytime if needed. + +For more information, see https://docs.github.com/en/repositories/archiving-a-github-repository/archiving-repositories#about-repository-archival + +If you think this is a mistake, please inform the Security team *ASAP* on Slack at `#github-appsec-security.` + +Thanks! :handshake:""", + repository=repo, + organization=organization, + token=token, + ) + if issue_res: + click.echo(f"{repo}... {issue_res}") + else: + click.echo(f"{repo}... Failure", err=True) + + if __name__ == "__main__": main() diff --git a/src/ghas_cli/utils/actions.py b/src/ghas_cli/utils/actions.py index f851c7b..af1fe79 100644 --- a/src/ghas_cli/utils/actions.py +++ b/src/ghas_cli/utils/actions.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- #!/usr/bin/env python3 -from typing import List import requests from . import network diff --git a/src/ghas_cli/utils/network.py b/src/ghas_cli/utils/network.py index b7557d3..81bdb16 100644 --- a/src/ghas_cli/utils/network.py +++ b/src/ghas_cli/utils/network.py @@ -31,7 +31,7 @@ def check_rate_limit(response: Any) -> bool: print( f"Rate limit reached: {response.headers['x-ratelimit-remaining']}/{response.headers['x-ratelimit-limit']} - {reset_time}" ) - time.sleep(reset_time) + time.sleep(int(response.headers["x-ratelimit-remaining"])) return True if response.status_code == 403: diff --git a/src/ghas_cli/utils/repositories.py b/src/ghas_cli/utils/repositories.py index 0d08040..691b5a0 100644 --- a/src/ghas_cli/utils/repositories.py +++ b/src/ghas_cli/utils/repositories.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- #!/usr/bin/env python3 -from typing import List +from typing import List, Any import base64 import requests from . import network import time +import datetime class Repository: @@ -216,6 +217,46 @@ def get_org_repositories( return repos_list +def get_default_branch_last_updated( + token: str, organization: str, repository_name: str, default_branch: str +) -> Any: + """ + Return the latest commit date on the default branch + """ + headers = network.get_github_headers(token) + + branch_res = requests.get( + url=f"https://api.github.com/repos/{organization}/{repository_name}/branches/{default_branch}", + headers=headers, + ) + + if branch_res.status_code != 200: + return False + + branch_res = branch_res.json() + + return datetime.datetime.strptime( + branch_res["commit"]["commit"]["author"]["date"].split("T")[0], "%Y-%m-%d" + ) + + +def archive(organization: str, token: str, repository: str) -> bool: + headers = network.get_github_headers(token) + + payload = {"archived": True} + + status = requests.patch( + url=f"https://api.github.com/repos/{organization}/{repository}", + headers=headers, + json=payload, + ) + + if status.status_code != 200: + return False + else: + return True + + def check_dependabot_alerts_enabled( token: str, organization: str, repository_name: str ) -> bool: