Skip to content

Commit 4524897

Browse files
authored
Merge branch 'main' into test/bar-chart
2 parents 074f35f + c0735d0 commit 4524897

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1450
-674
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ __pycache__
1515
.venv
1616
.vscode
1717
*.log
18+
*.pem
1819
backend/data
1920
backend/staticfiles
2021
build

.github/ansible/production/nest.yaml

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,30 @@
2828
shell:
2929
cmd: sed -i 's/\bnest-frontend\b/production-nest-frontend/' ~/frontend/Makefile
3030

31-
- name: Copy .env.backend
31+
- name: Copy secrets
3232
copy:
33-
src: '{{ github_workspace }}/.env.backend'
33+
src: '{{ github_workspace }}/{{ item }}'
3434
dest: ~/
3535
mode: '0400'
36+
loop:
37+
- .env.backend
38+
- .env.cache
39+
- .env.db
40+
- .env.frontend
41+
- .github.pem
3642

37-
- name: Copy .env.cache
38-
copy:
39-
src: '{{ github_workspace }}/.env.cache'
40-
dest: ~/
41-
mode: '0400'
42-
43-
- name: Copy .env.db
44-
copy:
45-
src: '{{ github_workspace }}/.env.db'
46-
dest: ~/
47-
mode: '0400'
48-
49-
- name: Copy .env.frontend
50-
copy:
51-
src: '{{ github_workspace }}/.env.frontend'
52-
dest: ~/
53-
mode: '0400'
43+
- name: Clean up secrets
44+
delegate_to: localhost
45+
file:
46+
path: '{{ github_workspace }}/{{ item }}'
47+
state: absent
48+
loop:
49+
- .env.backend
50+
- .env.cache
51+
- .env.db
52+
- .env.frontend
53+
- .github.pem
54+
run_once: true
5455

5556
- name: Copy crontab
5657
copy:

.github/ansible/staging/nest.yaml

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,28 @@
3434
state: directory
3535
mode: '0755'
3636

37-
- name: Copy .env.backend
37+
- name: Copy secrets
3838
copy:
39-
src: '{{ github_workspace }}/.env.backend'
39+
src: '{{ github_workspace }}/{{ item }}'
4040
dest: ~/
4141
mode: '0400'
42+
loop:
43+
- .env.backend
44+
- .env.cache
45+
- .env.db
46+
- .env.frontend
4247

43-
- name: Copy .env.cache
44-
copy:
45-
src: '{{ github_workspace }}/.env.cache'
46-
dest: ~/
47-
mode: '0400'
48-
49-
- name: Copy .env.db
50-
copy:
51-
src: '{{ github_workspace }}/.env.db'
52-
dest: ~/
53-
mode: '0400'
54-
55-
- name: Copy .env.frontend
56-
copy:
57-
src: '{{ github_workspace }}/.env.frontend'
58-
dest: ~/
59-
mode: '0400'
48+
- name: Clean up secrets
49+
delegate_to: localhost
50+
file:
51+
path: '{{ github_workspace }}/{{ item }}'
52+
state: absent
53+
loop:
54+
- .env.backend
55+
- .env.cache
56+
- .env.db
57+
- .env.frontend
58+
run_once: true
6059

6160
- name: Copy crontab
6261
copy:

.github/workflows/run-ci-cd.yaml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,9 @@ jobs:
286286
- name: Build backend image
287287
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
288288
with:
289+
build-args: |
290+
OWASP_GID=1001
291+
OWASP_UID=1001
289292
cache-from: |
290293
type=gha
291294
type=registry,ref=owasp/nest:backend-staging-cache
@@ -429,7 +432,6 @@ jobs:
429432
echo "DJANGO_SETTINGS_MODULE=${{ secrets.DJANGO_SETTINGS_MODULE }}" >> .env.backend
430433
echo "DJANGO_SLACK_BOT_TOKEN=${{ secrets.DJANGO_SLACK_BOT_TOKEN }}" >> .env.backend
431434
echo "DJANGO_SLACK_SIGNING_SECRET=${{ secrets.DJANGO_SLACK_SIGNING_SECRET }}" >> .env.backend
432-
echo "GITHUB_TOKEN=${{ secrets.NEST_GITHUB_TOKEN }}" >> .env.backend
433435
echo "SLACK_BOT_TOKEN_T04T40NHX=${{ secrets.SLACK_BOT_TOKEN_T04T40NHX }}" >> .env.backend
434436
435437
# Cache
@@ -516,6 +518,9 @@ jobs:
516518
- name: Build backend image
517519
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
518520
with:
521+
build-args: |
522+
OWASP_GID=1002
523+
OWASP_UID=1002
519524
cache-from: |
520525
type=gha
521526
type=registry,ref=owasp/nest:backend-staging-cache
@@ -652,6 +657,8 @@ jobs:
652657
echo "DJANGO_DB_PASSWORD=${{ secrets.DJANGO_DB_PASSWORD }}" >> .env.backend
653658
echo "DJANGO_DB_PORT=${{ secrets.DJANGO_DB_PORT }}" >> .env.backend
654659
echo "DJANGO_DB_USER=${{ secrets.DJANGO_DB_USER }}" >> .env.backend
660+
echo "DJANGO_GITHUB_APP_ID=${{ secrets.DJANGO_GITHUB_APP_ID }}" >> .env.backend
661+
echo "DJANGO_GITHUB_APP_INSTALLATION_ID=${{ secrets.DJANGO_GITHUB_APP_INSTALLATION_ID }}" >> .env.backend
655662
echo "DJANGO_OPEN_AI_SECRET_KEY=${{ secrets.DJANGO_OPEN_AI_SECRET_KEY }}" >> .env.backend
656663
echo "DJANGO_REDIS_HOST=${{ secrets.DJANGO_REDIS_HOST }}" >> .env.backend
657664
echo "DJANGO_REDIS_PASSWORD=${{ secrets.DJANGO_REDIS_PASSWORD }}" >> .env.backend
@@ -661,7 +668,6 @@ jobs:
661668
echo "DJANGO_SETTINGS_MODULE=${{ secrets.DJANGO_SETTINGS_MODULE }}" >> .env.backend
662669
echo "DJANGO_SLACK_BOT_TOKEN=${{ secrets.DJANGO_SLACK_BOT_TOKEN }}" >> .env.backend
663670
echo "DJANGO_SLACK_SIGNING_SECRET=${{ secrets.DJANGO_SLACK_SIGNING_SECRET }}" >> .env.backend
664-
echo "GITHUB_TOKEN=${{ secrets.NEST_GITHUB_TOKEN }}" >> .env.backend
665671
echo "SLACK_BOT_TOKEN_T04T40NHX=${{ secrets.SLACK_BOT_TOKEN_T04T40NHX }}" >> .env.backend
666672
667673
# Cache
@@ -684,6 +690,10 @@ jobs:
684690
echo "NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}" >> .env.frontend
685691
echo "NEXTAUTH_URL=${{ secrets.VITE_API_URL }}" >> .env.frontend
686692
693+
# GitHub App private key
694+
echo "${{ secrets.NEST_GITHUB_APP_PRIVATE_KEY }}" > .github.pem
695+
chmod 600 .github.pem
696+
687697
- name: Run Nest deploy
688698
working-directory: .github/ansible
689699
run: ansible-playbook -i inventory.yaml production/nest.yaml -e "github_workspace=$GITHUB_WORKSPACE"

backend/apps/github/auth.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""GitHub App authentication module."""
2+
3+
import logging
4+
import os
5+
from pathlib import Path
6+
7+
from django.conf import settings
8+
from github import Auth, Github
9+
from github.GithubException import BadCredentialsException
10+
11+
from apps.github.constants import GITHUB_ITEMS_PER_PAGE
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class GitHubAppAuth:
17+
"""GitHub App authentication handler."""
18+
19+
def __init__(self):
20+
"""Initialize GitHub App authentication."""
21+
self.app_id = settings.GITHUB_APP_ID
22+
self.app_installation_id = settings.GITHUB_APP_INSTALLATION_ID
23+
self.private_key = self._load_private_key()
24+
25+
self.pat_token = os.getenv("GITHUB_TOKEN")
26+
27+
if not self._is_app_configured() and not self.pat_token:
28+
error_message = (
29+
"GitHub App configuration is incomplete. "
30+
"Please set GITHUB_APP_ID and GITHUB_APP_INSTALLATION_ID, "
31+
"ensure backend/.github.pem file exists, "
32+
"or provide GITHUB_TOKEN for PAT authentication."
33+
)
34+
raise ValueError(error_message)
35+
36+
def _is_app_configured(self) -> bool:
37+
"""Check if GitHub App is properly configured."""
38+
return all((self.app_id, self.private_key, self.app_installation_id))
39+
40+
def _load_private_key(self):
41+
"""Load the GitHub App private key from a local file."""
42+
try:
43+
with (Path(settings.BASE_DIR) / ".github.pem").open("r") as key_file:
44+
return key_file.read().strip()
45+
except (FileNotFoundError, PermissionError):
46+
return None
47+
48+
def get_github_client(self, per_page: int | None = None) -> Github:
49+
"""Get authenticated GitHub client.
50+
51+
Args:
52+
per_page: Number of items per page for pagination.
53+
54+
Returns:
55+
Authenticated GitHub client instance.
56+
57+
Raises:
58+
BadCredentialsException: If authentication fails.
59+
60+
"""
61+
per_page = per_page or GITHUB_ITEMS_PER_PAGE
62+
63+
if self._is_app_configured():
64+
logger.warning("Using GitHub App authentication")
65+
return Github(
66+
auth=Auth.AppInstallationAuth(
67+
app_auth=Auth.AppAuth(
68+
app_id=self.app_id,
69+
private_key=self.private_key,
70+
),
71+
installation_id=int(self.app_installation_id),
72+
),
73+
per_page=per_page,
74+
)
75+
76+
if self.pat_token:
77+
logger.warning("Using GitHub PAT token")
78+
return Github(self.pat_token, per_page=per_page)
79+
80+
raise BadCredentialsException(401, "Invalid GitHub credentials", None)
81+
82+
83+
def get_github_client(per_page: int | None = None) -> Github:
84+
"""Get authenticated GitHub client.
85+
86+
Args:
87+
per_page: Number of items per page for pagination.
88+
89+
Returns:
90+
Authenticated GitHub client instance.
91+
92+
"""
93+
return GitHubAppAuth().get_github_client(per_page=per_page)

backend/apps/github/management/commands/github_add_related_repositories.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
"""A command to update OWASP entities related repositories data."""
22

33
import logging
4-
import os
54

6-
import github
75
from django.core.management.base import BaseCommand
86
from github.GithubException import UnknownObjectException
97

8+
from apps.github.auth import get_github_client
109
from apps.github.common import sync_repository
11-
from apps.github.constants import GITHUB_ITEMS_PER_PAGE
1210
from apps.github.utils import get_repository_path
1311
from apps.owasp.models.project import Project
1412

@@ -39,7 +37,7 @@ def handle(self, *args, **options) -> None:
3937
"""
4038
active_projects = Project.active_projects.order_by("-created_at")
4139
active_projects_count = active_projects.count()
42-
gh = github.Github(os.getenv("GITHUB_TOKEN"), per_page=GITHUB_ITEMS_PER_PAGE)
40+
gh = get_github_client()
4341

4442
offset = options["offset"]
4543
projects = []
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""A command to get GitHub App installation ID."""
2+
3+
import logging
4+
import os
5+
import sys
6+
from pathlib import Path
7+
8+
from django.conf import settings
9+
from django.core.management.base import BaseCommand
10+
from github import Auth, GithubIntegration
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class Command(BaseCommand):
16+
help = "Get GitHub App installation ID for the configured app."
17+
18+
def add_arguments(self, parser):
19+
"""Add command-line arguments to the parser.
20+
21+
Args:
22+
parser (argparse.ArgumentParser): The argument parser instance.
23+
24+
"""
25+
parser.add_argument(
26+
"--app-id",
27+
type=int,
28+
help="GitHub App ID (overrides GITHUB_APP_ID environment variable)",
29+
)
30+
parser.add_argument(
31+
"--private-key-file",
32+
type=str,
33+
help="Path to private key file (overrides default backend/.github.pem)",
34+
)
35+
36+
def handle(self, *args, **options):
37+
"""Handle the command execution.
38+
39+
Args:
40+
*args: Variable length argument list.
41+
**options: Arbitrary keyword arguments containing command options.
42+
43+
"""
44+
# Get app ID from arguments or environment
45+
app_id = options.get("app_id") or os.getenv("GITHUB_APP_ID")
46+
if not app_id:
47+
self.stderr.write(
48+
self.style.ERROR(
49+
"GitHub App ID is required. "
50+
"Provide --app-id argument or set GITHUB_APP_ID environment variable."
51+
)
52+
)
53+
sys.exit(1)
54+
55+
# Get private key from file
56+
private_key_file = (
57+
options.get("private_key_file") or Path(settings.BASE_DIR) / ".github.pem"
58+
)
59+
if not Path(private_key_file).exists():
60+
self.stderr.write(
61+
self.style.ERROR(
62+
f"Private key file not found: {private_key_file}. "
63+
"Please ensure the file exists and contains your GitHub App private key."
64+
)
65+
)
66+
sys.exit(1)
67+
68+
try:
69+
with Path(private_key_file).open("r") as key_file:
70+
private_key = key_file.read().strip()
71+
if not private_key:
72+
self.stderr.write(
73+
self.style.ERROR(f"Private key file is empty: {private_key_file}")
74+
)
75+
sys.exit(1)
76+
except (FileNotFoundError, PermissionError) as e:
77+
self.stderr.write(self.style.ERROR(f"Failed to read private key file: {e}"))
78+
sys.exit(1)
79+
80+
try:
81+
# Create GitHub App authentication
82+
app_auth = Auth.AppAuth(app_id=int(app_id), private_key=private_key)
83+
84+
# Create GitHub Integration instance
85+
gi = GithubIntegration(auth=app_auth)
86+
87+
# Get all installations
88+
installations = list(gi.get_installations())
89+
90+
if not installations:
91+
self.stdout.write(
92+
self.style.WARNING(f"No installations found for GitHub App ID: {app_id}")
93+
)
94+
return
95+
96+
self.stdout.write(
97+
self.style.SUCCESS(
98+
f"Found {len(installations)} installation(s) for GitHub App ID: {app_id}"
99+
)
100+
)
101+
102+
for installation in installations:
103+
self.stdout.write(f"Installation ID: {installation.id}")
104+
if hasattr(installation, "account") and installation.account:
105+
account_type = installation.account.type
106+
account_name = getattr(installation.account, "login", "N/A")
107+
self.stdout.write(f" Account: {account_name} ({account_type})")
108+
self.stdout.write("")
109+
110+
except Exception as e:
111+
self.stderr.write(self.style.ERROR(f"Failed to get installations: {e}"))
112+
logger.exception("Error getting GitHub App installations")
113+
sys.exit(1)

0 commit comments

Comments
 (0)