Skip to content

Commit 6dbd95e

Browse files
authored
Merge branch 'master' into release/2.44.4
2 parents cd9cf5f + 0077338 commit 6dbd95e

File tree

9 files changed

+291
-7
lines changed

9 files changed

+291
-7
lines changed

.dryrunsecurity.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ allowedAuthors:
6060
usernames:
6161
- mtesauro
6262
- devGregA
63-
- grendel513
6463
- cneill
6564
- Maffooch
6665
- blakeaowens
@@ -71,4 +70,3 @@ allowedAuthors:
7170
- valentijnscholten
7271
notificationList:
7372
- '@mtesauro'
74-
- '@grendel513'

.github/CODEOWNERS

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Any kind of package updates only need 2 approvals,
2+
# So let's add three folks here
3+
requirements.txt @cneill @mtesauro @Maffooch
4+
# Any dockerfile or compose changes will need to be viewed by
5+
# these people
6+
Dockerfile.* @mtesauro @Maffooch
7+
docker-compose.* @mtesauro @Maffooch
8+
/docker/ @mtesauro @Maffooch
9+
# Documentation changes
10+
/docs/ @paulOsinski @valentijnscholten @Maffooch
11+
# Kubernetes should be reviewed by reviewed first by those that know it
12+
/helm/ @cneill @kiblik
13+
# Anything UI related needs to be checked out by those with the eye for it
14+
/dojo/static/ @blakeaowens @Maffooch
15+
/dojo/templates/ @blakeaowens @Maffooch
16+
# Any model changes should be closely looked at
17+
/dojo/models.py @Maffooch

.github/pr-reminder.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import logging
2+
import os
3+
from datetime import datetime, timedelta
4+
5+
import requests
6+
7+
logger = logging.getLogger(__name__)
8+
9+
# Set up the GitHub and Slack tokens from environment variables
10+
GH_TOKEN = os.getenv("GH_TOKEN")
11+
SLACK_TOKEN = os.getenv("SLACK_TOKEN")
12+
REPO_OWNER = "DefectDojo"
13+
REPO_NAME = "django-DefectDojo"
14+
GITHUB_USER_NAME_TO_SLACK_EMAIL = {
15+
"Maffooch": "cody@defectdojo.com",
16+
"mtesauro": "matt@defectdojo.com",
17+
"devGregA": "greg@defectdojo.com",
18+
"blakeaowens": "blake@defectdojo.com",
19+
"dogboat": "sean@defectdojo.com",
20+
"cneill": "charles@defectdojo.com",
21+
"hblankenship": "harold@defectdojo.com",
22+
}
23+
24+
25+
# Helper function to calculate the prior Thursday from a given date
26+
def get_prior_thursday(date: datetime) -> str:
27+
# Calculate the day of the week (0=Monday, 1=Tuesday, ..., 6=Sunday)
28+
weekday = date.weekday()
29+
# If today is Thursday (weekday 3), return the same day.
30+
if weekday == 3:
31+
return date
32+
# Calculate how many days to subtract to reach the most recent Thursday
33+
days_to_subtract = (
34+
weekday - 3
35+
) % 7 # (weekday - 3) gives the number of days past Thursday
36+
prior_thursday = date - timedelta(days=days_to_subtract)
37+
38+
return prior_thursday.strftime("%Y-%m-%d")
39+
40+
41+
# Helper function to get Slack User ID from Slack Email
42+
def get_slack_user_id(slack_email: str) -> int:
43+
headers = {"Authorization": f"Bearer {SLACK_TOKEN}"}
44+
params = {"email": slack_email}
45+
response = requests.get(
46+
"https://slack.com/api/users.lookupByEmail",
47+
headers=headers,
48+
params=params,
49+
timeout=30,
50+
)
51+
52+
if response.status_code != 200 or not response.json().get("ok"):
53+
logger.info(f"Error fetching Slack user ID for email {slack_email}: {response.text}")
54+
return None
55+
56+
return response.json().get("user", {}).get("id")
57+
58+
59+
# Helper function to fetch pull requests from GitHub
60+
def get_pull_requests() -> dict:
61+
headers = {"Authorization": f"token {GH_TOKEN}"}
62+
response = requests.get(
63+
f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/pulls",
64+
headers=headers,
65+
timeout=30,
66+
)
67+
68+
if response.status_code != 200:
69+
logger.info(f"Error fetching PRs: {response.text}")
70+
response.raise_for_status()
71+
72+
return response.json()
73+
74+
75+
# Helper function to get PR reviews (approved, changes requested, or pending)
76+
def get_pr_reviews(pull_request: dict) -> list[dict]:
77+
pr_number = pull_request["number"]
78+
reviews_url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/pulls/{pr_number}/reviews"
79+
headers = {"Authorization": f"token {GH_TOKEN}"}
80+
response = requests.get(
81+
reviews_url,
82+
headers=headers,
83+
timeout=30,
84+
)
85+
86+
if response.status_code != 200:
87+
logger.info(f"Error fetching reviews for PR {pr_number}: {response.text}")
88+
return []
89+
90+
reviews = response.json()
91+
# Dictionary to store the latest review for each user
92+
latest_reviews = {}
93+
# Iterate over each review to find the latest one for each user
94+
for review in reviews:
95+
user = review["user"]["login"]
96+
submitted_at = review["submitted_at"]
97+
state = review["state"]
98+
# Convert the submitted_at timestamp to a datetime object for comparison
99+
review_time = datetime.strptime(submitted_at, "%Y-%m-%dT%H:%M:%SZ")
100+
# If the user doesn't have a review or the current one is later, update
101+
if user not in latest_reviews or (
102+
review_time > latest_reviews[user]["submitted_at"] and state != "COMMENTED"
103+
):
104+
latest_reviews[user] = {
105+
"user": user,
106+
"state": state,
107+
"submitted_at": review_time,
108+
"url": review["html_url"],
109+
}
110+
# Determine if there are any pending reviewers
111+
latest_reviews.update(
112+
{
113+
user_dict.get("login"): {
114+
"user": user_dict.get("login"),
115+
"state": "PENDING",
116+
}
117+
for user_dict in pull_request.get("requested_reviewers", [])
118+
},
119+
)
120+
# Return the latest review state and URL for each user
121+
return latest_reviews.values()
122+
123+
124+
# Helper function to send a message via Slack
125+
def send_slack_message(slack_user_id: int, message: str) -> None:
126+
headers = {
127+
"Content-Type": "application/json",
128+
"Authorization": f"Bearer {SLACK_TOKEN}",
129+
}
130+
payload = {"channel": slack_user_id, "text": message}
131+
response = requests.post(
132+
"https://slack.com/api/chat.postMessage",
133+
json=payload,
134+
headers=headers,
135+
timeout=30,
136+
)
137+
138+
if response.status_code != 200 or not response.json().get("ok"):
139+
logger.info(f"Error sending Slack message: {response.text}")
140+
response.raise_for_status()
141+
142+
143+
# Helper function to format the PR message with review statuses
144+
def format_pr_message(pull_request: dict, reviews: list[dict]) -> str:
145+
repo_name = pull_request["head"]["repo"]["name"]
146+
pull_request_title = pull_request["title"]
147+
pull_request_url = pull_request["html_url"]
148+
pull_request_number = pull_request["number"]
149+
constructed_title = f"{repo_name} (#{pull_request_number}): {pull_request_title}"
150+
message = f"• <{pull_request_url}|{constructed_title}>"
151+
# Fetch the milestone due date and URL
152+
if (milestone := pull_request.get("milestone")) is not None and (
153+
(milestone_due_date := milestone.get("due_on"))
154+
and (milestone_url := milestone.get("html_url"))
155+
and (milestone_title := milestone.get("title"))
156+
):
157+
message += f"\n Merge by: {get_prior_thursday(datetime.strptime(milestone_due_date, '%Y-%m-%dT%H:%M:%SZ'))} for release <{milestone_url}|{milestone_title}>"
158+
# Format reviews and append to the message (only latest review status per user)
159+
message += "\n Review Status:\n"
160+
for review in reviews:
161+
user = review["user"]
162+
state = review["state"]
163+
if url := review.get("url"):
164+
message += f" • {user}: <{url}|{state.lower().capitalize()}>\n"
165+
else:
166+
message += f" • {user}: {state.lower().capitalize()}\n"
167+
168+
return message
169+
170+
171+
# Main function to process PRs and notify Slack users
172+
def notify_reviewers():
173+
try:
174+
user_pr_map = {}
175+
slack_email_to_slack_id = {}
176+
pull_requests = get_pull_requests()
177+
# Logging all fetched PR details
178+
logger.info(f"Fetched {len(pull_requests)} PRs from GitHub.")
179+
for pull_request in pull_requests:
180+
title = pull_request["title"]
181+
pr_number = pull_request["number"]
182+
logger.info(f"Processing PR: {pr_number} - {title}")
183+
reviews = get_pr_reviews(pull_request)
184+
logger.info(f"Found {len(reviews)} reviews for PR {pr_number}.")
185+
message = format_pr_message(pull_request, reviews)
186+
# Map Slack users to PR messages
187+
for review in reviews:
188+
github_username = review["user"]
189+
if github_username not in user_pr_map:
190+
user_pr_map[github_username] = ""
191+
# Determine if we should prune any non pending reviews
192+
if f"{github_username}: Pending" in message:
193+
user_pr_map[github_username] += message + "\n"
194+
# Add the Header at the beginning of the list
195+
header_message = "Here are the PRs that are still requiring review:"
196+
# Add Tips and Tricks at the end of the list
197+
tips_message = "*Tips and Tricks*\n"
198+
tips_message += (
199+
"• This is how to remove a PR from the list: Approve, Request changes, or leave a general comment.\n"
200+
"• If someone else has requested changes, then leave a general comment to remove the pending review from yourself."
201+
)
202+
# Send Slack messages to reviewers
203+
for github_username, pr_list in user_pr_map.items():
204+
if pr_list:
205+
if slack_email := GITHUB_USER_NAME_TO_SLACK_EMAIL.get(github_username):
206+
if slack_user_id := slack_email_to_slack_id.get(
207+
slack_email,
208+
get_slack_user_id(slack_email),
209+
):
210+
message_content = f"Hello {github_username}! {header_message}\n{pr_list}\n{tips_message}"
211+
# logger.info("\n\n", message_content, "\n\n")
212+
send_slack_message(slack_user_id, message_content)
213+
except Exception as e:
214+
logger.info(f"Error occurred: {e}")
215+
raise
216+
217+
218+
if __name__ == "__main__":
219+
notify_reviewers()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Notify Pending PR Reviewers
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: '0 16 * * 1-5' # 11:00 AM CT M-F
7+
8+
jobs:
9+
notify-reviewers:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v2
15+
with:
16+
# Only checkout the master branch to avoid changes to this script
17+
# This is to reduce the possibilities of a secret leak from modifying
18+
# this action, or the python script that is called down below
19+
ref: master
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v2
23+
with:
24+
python-version: "3.12"
25+
26+
- name: Install dependencies
27+
run: |
28+
python -m pip install --upgrade pip
29+
pip install requests
30+
31+
- name: Run PR reminder script
32+
env:
33+
GH_TOKEN: ${{ secrets.GH_TOKEN }}
34+
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
35+
run: |
36+
python3 .github/pr-reminder.py

docs/content/en/changelog/changelog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ For Open Source release notes, please see the [Releases page on GitHub](https://
1010

1111
## Mar 2025: v2.44
1212

13+
### Mar 24, 2025, v2.44.3
14+
15+
- **(Import)** Generic Findings Import will now parse tags in the JSON payload when Async Import is enabled.
16+
17+
### Mar 17, 2025, v2.44.2
18+
19+
- **(Beta UI)** Added a new method to quickly assign permissions to Products or Product Types. See our [Pro Permissions](/en/customize_dojo/user_management/pro_permissions_overhaul/) for more details.
20+
21+
![image](images/pro_permissions_2.png)
22+
1323
### Mar 10, 2025: v2.44.1
1424

1525
- **(Beta UI)** Added a field in the View Engagement page which allows a user to navigate to the linked Jira Epic, if one exists.

docs/content/en/customize_dojo/user_management/audit_logging.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ In the Classic (Open-Source) UI, this history is found under the '☰' (hamburge
2424
This API returns 31 days of audit logs.
2525

2626
* Sending default or empty parameters will return the last 31 days of audit logs.
27+
2728
* Parameter `window_month` which will take a month and year in the format MM-YYYY and provide the audit logs for that month.
2829
* You can set the `window_start` parameter to limit these logs to a shorter window, rather than returning the entire month.
2930

docs/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
},
2222
"devDependencies": {
2323
"prettier": "3.5.2",
24-
"vite": "6.2.0"
24+
"vite": "6.2.3"
2525
},
2626
"engines": {
2727
"node": "22.14.0"

ruff.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ preview = true
109109
"S105", # hardcoded passwords in tests are fine
110110
"S108", # tmp paths mentioned in tests are fine
111111
]
112+
".github/pr-reminder.py" = [
113+
"INP001", # https://docs.astral.sh/ruff/rules/implicit-namespace-package/
114+
]
112115

113116
[lint.flake8-boolean-trap]
114117
extend-allowed-calls = ["dojo.utils.get_system_setting"]

0 commit comments

Comments
 (0)