Skip to content

Commit 1e3d7e5

Browse files
Merge pull request #25 from jitsecurity/sc-26600-add-uncover-assets
Add relevant script
2 parents f3002cc + 733133c commit 1e3d7e5

4 files changed

Lines changed: 328 additions & 0 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
## Script to uncover assets by github topic
2+
3+
This script allows automatically uncovering Jit Github repo assets that have a specified Github topic on them.
4+
5+
### Prerequisites
6+
7+
To run the script, you'll need to prepare:
8+
9+
- A Jit client & secret
10+
- The name of your Github organization
11+
- The name of the Github topic
12+
- A valid Github token with read permissions to your organization
13+
14+
### Quickstart
15+
16+
- Copy the `uncover_assets_by_topic.py` and `requirements.txt` files locally.
17+
- Run `pip install -r requirements.txt`
18+
- Set the following environment variables locally:
19+
```
20+
GITHUB_TOKEN=<your github token>
21+
GITHUB_ORGANIZATION=<your github org name>
22+
JIT_CLIENT_ID=<jit client>
23+
JIT_SECRET=<jit secret>
24+
TOPIC_TO_UNCOVER=<topic name to uncover by>
25+
```
26+
- Run `python uncover_asets_by_topic.py`
27+
28+
You should now see that the script runs successfully, and the relevat repos get uncovered from Jit. Note that organizations with a large number of repos can take a few minutes to complete.
29+
30+
### Running in Github actions
31+
32+
If you want to run the script from a Github action, choose a repository in the same organization and do the following:
33+
34+
1. Copy the `uncover_assets_by_topic.py` to the root of the repository
35+
2. Add the `JIT_CLIENT_ID` and `JIT_SECRET` as repository secret:
36+
37+
- To generate & add them to Github sercrets, use the [tutorial](https://docs.jit.io/docs/managing-users#generating-api-tokens) in our docs.
38+
- Make sure to name the Github secrets `JIT_CLIENT_ID` and `JIT_SECRET` in the same repo.
39+
- Create the following file in the repo: `.github/workflows/uncover_repos_by_topic.yml` with the following content:
40+
- Make sure to replace `<your topic name>` with the actual topic you want to use.
41+
42+
```
43+
name: Uncover Repos by Topic
44+
45+
on:
46+
workflow_dispatch:
47+
48+
jobs:
49+
run-script:
50+
runs-on: ubuntu-latest
51+
52+
steps:
53+
- name: Checkout repository
54+
uses: actions/checkout@v3
55+
56+
- name: Set up Python
57+
uses: actions/setup-python@v4
58+
with:
59+
python-version: "3.x" # Specify your Python version here
60+
61+
- name: Install dependencies
62+
run: |
63+
python -m pip install --upgrade pip
64+
python -m pip install requests
65+
python -m pip install PyGithub
66+
python -m pip install urllib3
67+
68+
- name: Run Python script
69+
env:
70+
JIT_CLIENT_ID: ${{ secrets.JIT_CLIENT_ID }}
71+
JIT_SECRET: ${{ secrets.JIT_SECRET }}
72+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN provided by GitHub Actions by default
73+
GITHUB_ORGANIZATION: ${{ github.repository_owner }}
74+
TOPIC_TO_UNCOVER: <your topic name>
75+
run: python uncover_assets_by_topic.py
76+
```
77+
78+
- You can now trigger this Github action via the actions tab in the Github repository, and the script should run successfully.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Deactivate Repos by Topic
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
run-script:
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- name: Checkout repository
12+
uses: actions/checkout@v3
13+
14+
- name: Set up Python
15+
uses: actions/setup-python@v4
16+
with:
17+
python-version: "3.x" # Specify your Python version here
18+
19+
- name: Install dependencies
20+
run: |
21+
python -m pip install --upgrade pip
22+
python -m pip install requests
23+
python -m pip install PyGithub
24+
python -m pip install urllib3
25+
26+
- name: Run Python script
27+
env:
28+
JIT_CLIENT_ID: ${{ secrets.JIT_CLIENT_ID }}
29+
JIT_SECRET: ${{ secrets.JIT_SECRET }}
30+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN provided by GitHub Actions by default
31+
GITHUB_ORGANIZATION: ${{ github.repository_owner }}
32+
TOPIC_TO_UNCOVER: <your topic name>
33+
run: python uncover_assets_by_topic.py
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
PyGithub==2.3.0
2+
requests==2.32.3
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import os
2+
import requests
3+
import time
4+
from github import Github, Auth
5+
from requests.adapters import HTTPAdapter
6+
from urllib3.util.retry import Retry
7+
import logging
8+
import sys
9+
10+
# Replace with your actual organization name
11+
TOPIC = os.getenv('TOPIC_TO_UNCOVER', '')
12+
ORGANIZATION = os.getenv('GITHUB_ORGANIZATION', '')
13+
14+
MAX_RETRIES = 5
15+
RETRY_BACKOFF_FACTOR = 2
16+
17+
18+
def main():
19+
logging.basicConfig(
20+
level=logging.INFO,
21+
format='%(asctime)s - %(levelname)s - %(message)s',
22+
stream=sys.stdout
23+
)
24+
logger = logging.getLogger(__name__)
25+
26+
assets_deactivated = AssetesDeactivate(logger=logger)
27+
assets_deactivated.run(TOPIC, ORGANIZATION)
28+
29+
30+
class AssetesDeactivate:
31+
def __init__(self, logger):
32+
self.logger = logger
33+
34+
def run(self, topic, org):
35+
self.logger.info(
36+
f"Fetching repositories with the {topic} topic in org {org}...")
37+
if not org:
38+
self.logger.error(
39+
"GITHUB_ORGANIZATION environment variable is not set.")
40+
exit(1)
41+
if not topic:
42+
self.logger.error(
43+
"TOPIC_TO_UNCOVER environment variable is not set.")
44+
exit(1)
45+
relevant_repos = self.get_topic_repos(topic, org)
46+
auth_token = self.jit_authentication()
47+
48+
# Check if token is valid for only 1 hour
49+
failed_repos = []
50+
for repo in relevant_repos:
51+
try:
52+
self.jit_deactivate_asset(repo, auth_token)
53+
except Exception as e:
54+
self.logger.error(
55+
f"Failed to deactivate asset for repository: {repo.name}. Error message: {str(e)}")
56+
failed_repos.append(repo.name)
57+
if failed_repos:
58+
self.logger.warn(
59+
f"Failed to deactivate assets for the following repositories: {failed_repos}")
60+
61+
def get_topic_repos(self, topic, org):
62+
github_token = os.getenv('GITHUB_TOKEN')
63+
if github_token == "" or github_token is None:
64+
self.logger.error("GITHUB_TOKEN environment variable is not set.")
65+
exit(1)
66+
67+
auth = Auth.Token(github_token)
68+
github_client = Github(auth=auth)
69+
70+
repos = github_client.get_organization(org).get_repos()
71+
# Can take a while, 10-20 seconds
72+
73+
filterd_repos = []
74+
for repo in repos:
75+
if topic in repo.topics:
76+
filterd_repos.append(repo)
77+
self.logger.info(
78+
f"Found {len(filterd_repos)} repositories with the {topic} topic in org {org}.")
79+
return filterd_repos
80+
81+
def jit_authentication(self):
82+
"""Authenticate with the JIT API and retrieve the access token."""
83+
auth_url = "https://api.jit.io/authentication/login"
84+
auth_payload = {
85+
"clientId": os.getenv('JIT_CLIENT_ID'),
86+
"secret": os.getenv('JIT_SECRET')
87+
}
88+
auth_headers = {
89+
"accept": "application/json",
90+
"content-type": "application/json"
91+
}
92+
93+
self.logger.info(
94+
f"Authenticating with JIT API using client ID: {auth_payload['clientId']}")
95+
response = self.send_request(
96+
url=auth_url, method="POST", headers=auth_headers, json=auth_payload)
97+
98+
if response.status_code == 200:
99+
token = response.json().get('accessToken')
100+
self.logger.info("Authentication successful.")
101+
return token
102+
else:
103+
self.logger.error("Authentication failed. Exiting.")
104+
exit(1)
105+
106+
def jit_deactivate_asset(self, repo, token):
107+
repo_name = repo.name
108+
self.logger.info(f"Processing repository: {repo_name}")
109+
110+
asset_url = f"https://api.jit.io/asset/type/repo/vendor/github/owner/{repo.owner.login}/name/{repo_name}"
111+
asset_headers = {
112+
"accept": "application/json",
113+
"authorization": f"Bearer {token}"
114+
}
115+
116+
self.logger.info(f"Fetching asset for repository: {repo_name}")
117+
asset_response = self.send_request(
118+
url=asset_url, headers=asset_headers)
119+
self.logger.info(
120+
f"Asset response status for {repo_name}: {asset_response.status_code}")
121+
122+
if asset_response.status_code != 200:
123+
self.logger.error(
124+
f"Failed to fetch asset for repository {repo_name}!")
125+
return
126+
asset = asset_response.json()
127+
128+
if asset.get('is_covered') is False:
129+
self.logger.info(
130+
f"Asset is already deactivated for repository: {repo_name}. Skipping...")
131+
return
132+
self.deactivate_asset(updated_asset=asset, token=token)
133+
self.logger.info(f"Asset deactivated for repository: {repo_name}")
134+
135+
def deactivate_asset(self, updated_asset, token):
136+
update_url = f"https://api.jit.io/asset/asset/{updated_asset['asset_id']}"
137+
asset_headers = {
138+
"accept": "application/json",
139+
"authorization": f"Bearer {token}"
140+
}
141+
fields_to_update = {
142+
# "is_active": False,
143+
"is_covered": False
144+
}
145+
146+
self.logger.info(
147+
f"Updating asset: {updated_asset['asset_id']} with data: {fields_to_update}")
148+
update_response = self.send_request(
149+
url=update_url, method="PATCH", headers=asset_headers, json=fields_to_update)
150+
self.logger.info(
151+
f"Update response status: {update_response.status_code}")
152+
153+
def retry_request(func):
154+
"""Decorator to retry a request in case of failure."""
155+
156+
def wrapper(self, *args, **kwargs):
157+
retries = MAX_RETRIES
158+
backoff_factor = RETRY_BACKOFF_FACTOR
159+
response = None
160+
for attempt in range(retries):
161+
response = func(*args, **kwargs)
162+
if response.status_code == 200:
163+
return response
164+
elif response.status_code == 401:
165+
self.logger.warn(
166+
"Unauthorized. The token might be expired. Re-authenticating...")
167+
# Re-authenticate and update the token
168+
main.auth_token = self.jit_authentication()
169+
kwargs['headers']['authorization'] = f"Bearer {main.auth_token}"
170+
elif response.status_code == 429:
171+
wait_time = (attempt + 1) * backoff_factor
172+
self.logger.warn(
173+
f"Rate limit hit. Retrying in {wait_time} seconds...")
174+
time.sleep(wait_time)
175+
elif response.status_code == 403:
176+
self.logger.error("Access is forbidden.")
177+
if "rate limit" in response.text.lower():
178+
wait_time = (attempt + 1) * backoff_factor
179+
self.logger.warn(
180+
f"Rate limit possibly hit. Retrying in {wait_time} seconds...")
181+
time.sleep(wait_time)
182+
else:
183+
self.logger.error(
184+
"Access is permanently forbidden. Exiting.")
185+
exit(1)
186+
else:
187+
self.logger.warn(
188+
f"Request failed with status code {response.status_code}. Retrying...")
189+
wait_time = (attempt + 1) * backoff_factor
190+
time.sleep(wait_time)
191+
192+
self.logger.error(
193+
f"Max retries reached. Request failed for URL: {args[0]}")
194+
return response
195+
return wrapper
196+
197+
@ retry_request
198+
def send_request(url, method="GET", headers=None, json=None, params=None):
199+
session = requests.Session()
200+
retry = Retry(total=5, backoff_factor=1,
201+
status_forcelist=[429, 500, 502, 503, 504])
202+
adapter = HTTPAdapter(max_retries=retry)
203+
session.mount('https://', adapter)
204+
session.mount('http://', adapter)
205+
206+
if method == "GET":
207+
return session.get(url, headers=headers, params=params)
208+
elif method == "PATCH":
209+
return session.patch(url, headers=headers, json=json)
210+
elif method == "POST":
211+
return session.post(url, headers=headers, json=json)
212+
213+
214+
if __name__ == "__main__":
215+
main()

0 commit comments

Comments
 (0)