From 459ef1ab5ac27a28ed93df32b2632fbc7ea51167 Mon Sep 17 00:00:00 2001 From: Yogesh Ojha Date: Thu, 5 Sep 2024 07:07:53 +0530 Subject: [PATCH] fix errors when api keys are not set --- web/api/shared_api_tasks.py | 14 ++- web/api/views.py | 202 ++++++++++++++++++++------------- web/dashboard/admin.py | 1 + web/static/custom/bountyhub.js | 54 +++++---- 4 files changed, 169 insertions(+), 102 deletions(-) diff --git a/web/api/shared_api_tasks.py b/web/api/shared_api_tasks.py index 04876e8d..1d17f25a 100644 --- a/web/api/shared_api_tasks.py +++ b/web/api/shared_api_tasks.py @@ -22,7 +22,12 @@ def import_hackerone_programs_task(handles, project_slug, is_sync = False): def fetch_program_details_from_hackerone(program_handle): url = f'https://api.hackerone.com/v1/hackers/programs/{program_handle}' headers = {'Accept': 'application/json'} - username, api_key = get_hackerone_key_username() + creds = get_hackerone_key_username() + + if not creds: + raise Exception("HackerOne API credentials not configured") + + username, api_key = creds response = requests.get( url, @@ -132,7 +137,12 @@ def sync_bookmarked_programs_task(project_slug): url = f'https://api.hackerone.com/v1/hackers/programs?&page[size]=100' headers = {'Accept': 'application/json'} bookmarked_programs = [] - username, api_key = get_hackerone_key_username() + + credentials = get_hackerone_key_username() + if not credentials: + raise Exception("HackerOne API credentials not configured") + + username, api_key = credentials while url: response = requests.get( diff --git a/web/api/views.py b/web/api/views.py index 4fd27fd0..7efe0b59 100644 --- a/web/api/views.py +++ b/web/api/views.py @@ -37,6 +37,15 @@ from .serializers import * logger = logging.getLogger(__name__) +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response +from django.core.cache import cache +from django.core.exceptions import ObjectDoesNotExist +from datetime import datetime +import requests + + class HackerOneProgramViewSet(viewsets.ViewSet): """ This class manages the HackerOne Program model, @@ -51,25 +60,28 @@ class HackerOneProgramViewSet(viewsets.ViewSet): ALLOWED_ASSET_TYPES = ["WILDCARD", "DOMAIN", "IP_ADDRESS", "CIDR", "URL"] def list(self, request): - sort_by = request.query_params.get('sort_by', 'age') - sort_order = request.query_params.get('sort_order', 'desc') # Changed default to 'desc' + try: + sort_by = request.query_params.get('sort_by', 'age') + sort_order = request.query_params.get('sort_order', 'desc') - programs = self.get_cached_programs() + programs = self.get_cached_programs() - if sort_by == 'name': - programs = sorted(programs, key=lambda x: x['attributes']['name'].lower(), - reverse=(sort_order.lower() == 'desc')) - elif sort_by == 'reports': - programs = sorted(programs, key=lambda x: x['attributes'].get('number_of_reports_for_user', 0), - reverse=(sort_order.lower() == 'desc')) - elif sort_by == 'age': - programs = sorted(programs, - key=lambda x: datetime.strptime(x['attributes'].get('started_accepting_at', '1970-01-01T00:00:00.000Z'), '%Y-%m-%dT%H:%M:%S.%fZ'), - reverse=(sort_order.lower() == 'desc') - ) + if sort_by == 'name': + programs = sorted(programs, key=lambda x: x['attributes']['name'].lower(), + reverse=(sort_order.lower() == 'desc')) + elif sort_by == 'reports': + programs = sorted(programs, key=lambda x: x['attributes'].get('number_of_reports_for_user', 0), + reverse=(sort_order.lower() == 'desc')) + elif sort_by == 'age': + programs = sorted(programs, + key=lambda x: datetime.strptime(x['attributes'].get('started_accepting_at', '1970-01-01T00:00:00.000Z'), '%Y-%m-%dT%H:%M:%S.%fZ'), + reverse=(sort_order.lower() == 'desc') + ) - serializer = HackerOneProgramSerializer(programs, many=True) - return Response(serializer.data) + serializer = HackerOneProgramSerializer(programs, many=True) + return Response(serializer.data) + except Exception as e: + return self.handle_exception(e) def get_api_credentials(self): try: @@ -82,18 +94,24 @@ class HackerOneProgramViewSet(viewsets.ViewSet): @action(detail=False, methods=['get']) def bookmarked_programs(self, request): - # do not cache bookmarked programs due to the user specific nature - programs = self.fetch_programs_from_hackerone() - bookmarked = [p for p in programs if p['attributes']['bookmarked']] - serializer = HackerOneProgramSerializer(bookmarked, many=True) - return Response(serializer.data) + try: + # do not cache bookmarked programs due to the user specific nature + programs = self.fetch_programs_from_hackerone() + bookmarked = [p for p in programs if p['attributes']['bookmarked']] + serializer = HackerOneProgramSerializer(bookmarked, many=True) + return Response(serializer.data) + except Exception as e: + return self.handle_exception(e) @action(detail=False, methods=['get']) def bounty_programs(self, request): - programs = self.get_cached_programs() - bounty_programs = [p for p in programs if p['attributes']['offers_bounties']] - serializer = HackerOneProgramSerializer(bounty_programs, many=True) - return Response(serializer.data) + try: + programs = self.get_cached_programs() + bounty_programs = [p for p in programs if p['attributes']['offers_bounties']] + serializer = HackerOneProgramSerializer(bounty_programs, many=True) + return Response(serializer.data) + except Exception as e: + return self.handle_exception(e) def get_cached_programs(self): programs = cache.get(self.CACHE_KEY) @@ -106,7 +124,10 @@ class HackerOneProgramViewSet(viewsets.ViewSet): url = f'{self.API_BASE}/programs?page[size]=100' headers = {'Accept': 'application/json'} all_programs = [] - username, api_key = self.get_api_credentials() + try: + username, api_key = self.get_api_credentials() + except Exception as e: + raise Exception("API credentials error: " + str(e)) while url: response = requests.get( @@ -115,7 +136,9 @@ class HackerOneProgramViewSet(viewsets.ViewSet): auth=(username, api_key) ) - if response.status_code != 200: + if response.status_code == 401: + raise Exception("Invalid API credentials") + elif response.status_code != 200: raise Exception(f"HackerOne API request failed with status code {response.status_code}") data = response.json() @@ -127,38 +150,46 @@ class HackerOneProgramViewSet(viewsets.ViewSet): @action(detail=False, methods=['post']) def refresh_cache(self, request): - programs = self.fetch_programs_from_hackerone() - cache.set(self.CACHE_KEY, programs, self.CACHE_TIMEOUT) - return Response({"status": "Cache refreshed successfully"}) + try: + programs = self.fetch_programs_from_hackerone() + cache.set(self.CACHE_KEY, programs, self.CACHE_TIMEOUT) + return Response({"status": "Cache refreshed successfully"}) + except Exception as e: + return self.handle_exception(e) @action(detail=True, methods=['get']) def program_details(self, request, pk=None): - program_handle = pk - cache_key = self.PROGRAM_CACHE_KEY.format(program_handle) - program_details = cache.get(cache_key) + try: + program_handle = pk + cache_key = self.PROGRAM_CACHE_KEY.format(program_handle) + program_details = cache.get(cache_key) + + if program_details is None: + program_details = self.fetch_program_details_from_hackerone(program_handle) + if program_details: + cache.set(cache_key, program_details, self.CACHE_TIMEOUT) - if program_details is None: - program_details = self.fetch_program_details_from_hackerone(program_handle) if program_details: - cache.set(cache_key, program_details, self.CACHE_TIMEOUT) + filtered_scopes = [ + scope for scope in program_details.get('relationships', {}).get('structured_scopes', {}).get('data', []) + if scope.get('attributes', {}).get('asset_type') in self.ALLOWED_ASSET_TYPES + ] - if program_details: - filtered_scopes = [ - scope for scope in program_details.get('relationships', {}).get('structured_scopes', {}).get('data', []) - if scope.get('attributes', {}).get('asset_type') in self.ALLOWED_ASSET_TYPES - ] + program_details['relationships']['structured_scopes']['data'] = filtered_scopes - program_details['relationships']['structured_scopes']['data'] = filtered_scopes - - return Response(program_details) - else: - return Response({"error": "Program not found"}, status=404) - + return Response(program_details) + else: + return Response({"error": "Program not found"}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + return self.handle_exception(e) def fetch_program_details_from_hackerone(self, program_handle): url = f'{self.API_BASE}/programs/{program_handle}' headers = {'Accept': 'application/json'} - username, api_key = self.get_api_credentials() + try: + username, api_key = self.get_api_credentials() + except Exception as e: + raise Exception("API credentials error: " + str(e)) response = requests.get( url, @@ -166,53 +197,68 @@ class HackerOneProgramViewSet(viewsets.ViewSet): auth=(username, api_key) ) - if response.status_code == 200: + if response.status_code == 401: + raise Exception("Invalid API credentials") + elif response.status_code == 200: return response.json() else: return None @action(detail=False, methods=['post']) def import_programs(self, request): - project_slug = request.query_params.get('project_slug') - if not project_slug: - return Response({"error": "Project slug is required"}, status=HTTP_400_BAD_REQUEST) - handles = request.data.get('handles', []) + try: + project_slug = request.query_params.get('project_slug') + if not project_slug: + return Response({"error": "Project slug is required"}, status=status.HTTP_400_BAD_REQUEST) + handles = request.data.get('handles', []) - if not handles: - return Response({"error": "No program handles provided"}, status=HTTP_400_BAD_REQUEST) + if not handles: + return Response({"error": "No program handles provided"}, status=status.HTTP_400_BAD_REQUEST) - import_hackerone_programs_task.delay(handles, project_slug) + import_hackerone_programs_task.delay(handles, project_slug) - create_inappnotification( - title="HackerOne Program Import Started", - description=f"Import process for {len(handles)} program(s) has begun.", - notification_type=PROJECT_LEVEL_NOTIFICATION, - project_slug=project_slug, - icon="mdi-download", - status='info' - ) + create_inappnotification( + title="HackerOne Program Import Started", + description=f"Import process for {len(handles)} program(s) has begun.", + notification_type=PROJECT_LEVEL_NOTIFICATION, + project_slug=project_slug, + icon="mdi-download", + status='info' + ) - return Response({"message": f"Import process for {len(handles)} program(s) has begun."}, status=HTTP_202_ACCEPTED) + return Response({"message": f"Import process for {len(handles)} program(s) has begun."}, status=status.HTTP_202_ACCEPTED) + except Exception as e: + return self.handle_exception(e) @action(detail=False, methods=['get']) def sync_bookmarked(self, request): - project_slug = request.query_params.get('project_slug') - if not project_slug: - return Response({"error": "Project slug is required"}, status=HTTP_400_BAD_REQUEST) + try: + project_slug = request.query_params.get('project_slug') + if not project_slug: + return Response({"error": "Project slug is required"}, status=status.HTTP_400_BAD_REQUEST) - sync_bookmarked_programs_task.delay(project_slug) + sync_bookmarked_programs_task.delay(project_slug) - create_inappnotification( - title="HackerOne Bookmarked Programs Sync Started", - description="Sync process for bookmarked programs has begun.", - notification_type=PROJECT_LEVEL_NOTIFICATION, - project_slug=project_slug, - icon="mdi-sync", - status='info' - ) + create_inappnotification( + title="HackerOne Bookmarked Programs Sync Started", + description="Sync process for bookmarked programs has begun.", + notification_type=PROJECT_LEVEL_NOTIFICATION, + project_slug=project_slug, + icon="mdi-sync", + status='info' + ) - return Response({"message": "Sync process for bookmarked programs has begun."}, status=HTTP_202_ACCEPTED) + return Response({"message": "Sync process for bookmarked programs has begun."}, status=status.HTTP_202_ACCEPTED) + except Exception as e: + return self.handle_exception(e) + def handle_exception(self, exc): + if isinstance(exc, ObjectDoesNotExist): + return Response({"error": "HackerOne API credentials not configured"}, status=status.HTTP_503_SERVICE_UNAVAILABLE) + elif str(exc) == "Invalid API credentials": + return Response({"error": "Invalid HackerOne API credentials"}, status=status.HTTP_401_UNAUTHORIZED) + else: + return Response({"error": str(exc)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class InAppNotificationManagerViewSet(viewsets.ModelViewSet): """ diff --git a/web/dashboard/admin.py b/web/dashboard/admin.py index 19aa7080..d7e51352 100644 --- a/web/dashboard/admin.py +++ b/web/dashboard/admin.py @@ -6,4 +6,5 @@ admin.site.register(Project) admin.site.register(OpenAiAPIKey) admin.site.register(NetlasAPIKey) admin.site.register(ChaosAPIKey) +admin.site.register(HackerOneAPIKey) admin.site.register(InAppNotification) \ No newline at end of file diff --git a/web/static/custom/bountyhub.js b/web/static/custom/bountyhub.js index d11fa719..6cc7985e 100644 --- a/web/static/custom/bountyhub.js +++ b/web/static/custom/bountyhub.js @@ -28,16 +28,16 @@ document.addEventListener('DOMContentLoaded', function() { } else { showLoadingIndicator("Loading HackerOne Programs"); } - + let api_url = isBookmarkedRequest ? '/api/hackerone-programs/bookmarked_programs/' : '/api/hackerone-programs/'; - + const sortParams = updateSortingParams(); const queryParams = new URLSearchParams(sortParams).toString(); - + if (queryParams) { api_url += '?' + queryParams; } - + try { const response = await fetch(api_url, { method: "GET", @@ -46,17 +46,23 @@ document.addEventListener('DOMContentLoaded', function() { "X-CSRFToken": getCookie("csrftoken"), }, }); - + if (!response.ok) { - throw new Error('Network response was not ok'); + const errorData = await response.json(); + throw new Error(errorData.error || 'An error occurred while fetching the programs.'); } - + const data = await response.json(); allPrograms = data; displayPrograms(data); } catch (error) { - displayErrorMessage("An error occurred while fetching the hackerone programs. Please try again later. Make sure you have hackerone api key set in your API Vault."); console.error('Error:', error); + Swal.fire({ + icon: 'error', + title: 'Error', + text: error.message, + confirmButtonText: 'OK' + }); } finally { hideLoadingIndicator(); } @@ -263,7 +269,10 @@ document.addEventListener('DOMContentLoaded', function() { } function hideLoadingIndicator() { - Swal.close(); + // Only close the Swal if it's a loading indicator + if (Swal.isVisible() && Swal.getTitle().textContent.includes('Loading')) { + Swal.close(); + } } fetchPrograms(false, false); @@ -310,7 +319,9 @@ function see_detail(handle) { fetch(`/api/hackerone-programs/${handle}/program_details/`) .then(response => { if (!response.ok) { - throw new Error('Network response was not ok'); + return response.json().then(errorData => { + throw new Error(errorData.error || 'An error occurred while fetching program details.'); + }); } return response.json(); }) @@ -321,11 +332,10 @@ function see_detail(handle) { }) .catch(error => { console.error('Error:', error); - Swal.fire({ icon: 'error', - title: 'Oops...', - text: 'There was an error fetching the program details. Please try again.', + title: 'Error', + text: error.message, }); }); } @@ -531,7 +541,8 @@ async function importPrograms(handles) { }); if (!response.ok) { - throw new Error('Import failed'); + const errorData = await response.json(); + throw new Error(errorData.error || 'Import failed'); } return await response.json(); @@ -542,7 +553,6 @@ async function importPrograms(handles) { } function handleProgramImportswal(handles) { - // swal loader to handle the import Swal.fire({ title: 'Confirm Import', html: ` @@ -558,11 +568,11 @@ function handleProgramImportswal(handles) { }).then((result) => { if (result.isConfirmed) { importPrograms(handles) - .then(() => { + .then((response) => { Swal.fire({ title: 'Import Started', html: ` -

The import process for ${handles.length} program(s) has begun.

+

${response.message}

You will receive notifications about the progress and completion of the import.

`, icon: 'info', @@ -570,18 +580,18 @@ function handleProgramImportswal(handles) { confirmButtonColor: '#3085d6', }); }) - .catch(() => { + .catch((error) => { Swal.fire({ title: 'Import Initiation Failed', - text: 'There was an error starting the import process. Please try again.', + text: error.message, icon: 'error', confirmButtonText: 'OK', confirmButtonColor: '#3085d6', }); }); - // clear all selected cards - const container = document.getElementById('program_cards'); - container.querySelectorAll('.card-selected').forEach(card => card.classList.remove('card-selected')); + // clear all selected cards + const container = document.getElementById('program_cards'); + container.querySelectorAll('.card-selected').forEach(card => card.classList.remove('card-selected')); } }); } \ No newline at end of file