From 984e75ba4aa6f9948281f13ae3b831c341584113 Mon Sep 17 00:00:00 2001 From: vapor-forensics Date: Fri, 7 Feb 2025 15:45:58 +1000 Subject: [PATCH 1/2] dev --- apps/aws/views.py | 6 +- apps/data/urls.py | 10 ++ apps/data/views.py | 50 +++++- scope/urls.py | 1 + templates/aws/account_resources.html | 144 +++++++----------- .../{normalized_logs.html => get_logs.html} | 6 + templates/data/normalized_logs.html | 98 ++++++++++++ 7 files changed, 219 insertions(+), 96 deletions(-) create mode 100644 apps/data/urls.py rename templates/aws/{normalized_logs.html => get_logs.html} (96%) create mode 100644 templates/data/normalized_logs.html diff --git a/apps/aws/views.py b/apps/aws/views.py index 9c93512..c5e2f65 100644 --- a/apps/aws/views.py +++ b/apps/aws/views.py @@ -179,7 +179,7 @@ def normalized_logs_view(request, account_id): "start_date": start_date, "end_date": end_date, } - return render(request, "aws/normalized_logs.html", context) + return render(request, "aws/get_logs.html", context) @login_required def aws_logsource_details(request, slug): @@ -244,7 +244,7 @@ def fetch_cloudtrail_logs(request, account_id): case_id=resource.case.id ) messages.success(request, "CloudTrail log fetching has been queued.") - return redirect("aws:normalized_logs", account_id=aws_account.account_id) + return redirect("aws:get_logs", account_id=aws_account.account_id) else: form = FetchCloudTrailLogsForm() @@ -260,7 +260,7 @@ def trigger_management_event_fetch(request, account_id): messages.success(request, "Management event history is being fetched.") logger.info(f"Task queued for AWS account {account_id}") - return redirect("aws:normalized_logs", account_id=aws_account.account_id) + return redirect("aws:get_logs", account_id=aws_account.account_id) @login_required def aws_credential_details(request, slug): diff --git a/apps/data/urls.py b/apps/data/urls.py new file mode 100644 index 0000000..f02d7cc --- /dev/null +++ b/apps/data/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from django.views.generic import TemplateView + +from . import views + +app_name = 'data' + +urlpatterns = [ + path("logs", views.NormalizedLogListView, name="normalized_logs"), +] \ No newline at end of file diff --git a/apps/data/views.py b/apps/data/views.py index 91ea44a..e95b8d1 100644 --- a/apps/data/views.py +++ b/apps/data/views.py @@ -1,3 +1,51 @@ from django.shortcuts import render +from django.contrib.auth.decorators import login_required +from django.core.paginator import Paginator +from django.db.models import Q +from apps.data.models import NormalizedLog -# Create your views here. +@login_required +def NormalizedLogListView(request): + # Get all logs and apply default sorting + queryset = NormalizedLog.objects.all().order_by('-event_time') + + # Get filter parameters from request + search_query = request.GET.get('search', '') + field_filter = request.GET.get('field', '') + field_value = request.GET.get('field_value', '') + sort_order = request.GET.get('sort', '-event_time') + + # Apply search if query exists + if search_query: + queryset = queryset.filter( + Q(event_name__icontains=search_query) | + Q(event_source__icontains=search_query) | + Q(event_type__icontains=search_query) | + Q(user_identity__icontains=search_query) | + Q(resources__icontains=search_query) + ) + + # Apply field-specific filter if specified + if field_filter and field_value: + filter_kwargs = {f"{field_filter}__icontains": field_value} + queryset = queryset.filter(**filter_kwargs) + + # Apply sorting + queryset = queryset.order_by(sort_order.replace('timestamp', 'event_time')) + + # Pagination + paginator = Paginator(queryset, 50) # Show 50 logs per page + page_number = request.GET.get('page', 1) + page_obj = paginator.get_page(page_number) + + context = { + 'object_list': page_obj, + 'page_obj': page_obj, + 'search_query': search_query, + 'field_filter': field_filter, + 'field_value': field_value, + 'sort_order': sort_order, + 'is_paginated': page_obj.has_other_pages(), + } + + return render(request, 'data/normalized_logs.html', context) diff --git a/scope/urls.py b/scope/urls.py index f7a9a64..9e42f33 100644 --- a/scope/urls.py +++ b/scope/urls.py @@ -38,6 +38,7 @@ path("", include("apps.web.urls")), path("case/", include("apps.case.urls")), path("aws/", include("apps.aws.urls")), + path("data/", include("apps.data.urls")), path("celery-progress/", include("celery_progress.urls")), # API docs path("api/schema/", SpectacularAPIView.as_view(), name="schema"), diff --git a/templates/aws/account_resources.html b/templates/aws/account_resources.html index 17645d4..1aeba2d 100644 --- a/templates/aws/account_resources.html +++ b/templates/aws/account_resources.html @@ -29,6 +29,55 @@

Account Details for: {{ aws_account.account_id }}

+ +
+

IAM Users

+ + {% if aws_credentials %} +
+ {% for credential in aws_credentials %} +
+
+
+

{{ credential.user }}

+
+
+ Password: + {% if credential.password_enabled %} + Enabled + {% else %} + Disabled + {% endif %} +
+
+ MFA: + {% if credential.mfa_active %} + Active + {% else %} + Inactive + {% endif %} +
+
+ Created: + {{ credential.user_creation_time|date|default:"N/A" }} +
+
+ + View Details + +
+
+
+ {% endfor %} +
+ {% else %} +
+

No IAM users found.

+
+ {% endif %} +
+

AWS Resources

@@ -70,7 +119,7 @@

-
+

AWS Log Sources

{% for message in error_messages %} @@ -107,96 +156,7 @@

{{ log_source.log_name }}

{% endif %}
- - -
-
-
- -
-
-
-
-
- - - - - - - - - - - - - - {% for credential in aws_credentials %} - - - - - - - - - - {% empty %} - - - - {% endfor %} - -
UsernameCreatedPassword EnabledMFA ActiveAccess Key 1Access Key 2Last Activity
- - {{ credential.user }} - - {{ credential.user_creation_time|default:"N/A" }} - {% if credential.password_enabled %} - Yes - {% else %} - No - {% endif %} - - {% if credential.mfa_active %} - Yes - {% else %} - No - {% endif %} - - {% if credential.access_key_1_active %} - Active - {% if credential.access_key_1_last_used_date %} -
Last used: {{ credential.access_key_1_last_used_date|date }} - {% endif %} - {% else %} - Inactive - {% endif %} -
- {% if credential.access_key_2_active %} - Active - {% if credential.access_key_2_last_used_date %} -
Last used: {{ credential.access_key_2_last_used_date|date }} - {% endif %} - {% else %} - Inactive - {% endif %} -
- {% if credential.password_last_used %} - Password: {{ credential.password_last_used|date }}
- {% endif %} - {% if credential.access_key_1_last_used_date or credential.access_key_2_last_used_date %} - {% with last_key_use=credential.access_key_1_last_used_date|default:credential.access_key_2_last_used_date %} - Access Key: {{ last_key_use|date }} - {% endwith %} - {% endif %} -
No credentials found
-
-
-
-
+ +
{% endblock %} diff --git a/templates/aws/normalized_logs.html b/templates/aws/get_logs.html similarity index 96% rename from templates/aws/normalized_logs.html rename to templates/aws/get_logs.html index b175ecd..6f28311 100644 --- a/templates/aws/normalized_logs.html +++ b/templates/aws/get_logs.html @@ -114,5 +114,11 @@

Top 10 Events

+ +
+ + View Normalized Logs + +
{% endblock %} diff --git a/templates/data/normalized_logs.html b/templates/data/normalized_logs.html new file mode 100644 index 0000000..8c16140 --- /dev/null +++ b/templates/data/normalized_logs.html @@ -0,0 +1,98 @@ +{% extends "web/app/app_base.html" %} +{% load static %} + +{% block content %} +
+

Normalized Logs

+ + +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ + +
+ + + + + + + + + + + + {% for log in object_list %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
TimestampLevelSourceResource IDMessage
{{ log.timestamp }} + {{ log.log_level }} + {{ log.source }}{{ log.resource_id }}{{ log.message }}
No logs found.
+
+ + + {% if is_paginated %} + + {% endif %} +
+{% endblock %} \ No newline at end of file From a7016526e752b4a4687be2ee56091ac8a3097e6a Mon Sep 17 00:00:00 2001 From: vapor-forensics Date: Sun, 16 Feb 2025 19:43:14 +1000 Subject: [PATCH 2/2] AWS AAWS integration update and front end design updates. --- README_NEW.md | 247 +++++++++ apps/analysis/detection_rules/aws_rules.yaml | 41 ++ apps/analysis/detections.py | 87 ++++ apps/analysis/forms.py | 21 + .../commands/load_detection_rules.py | 66 +++ apps/analysis/migrations/0001_initial.py | 34 ++ apps/analysis/models.py | 48 +- apps/analysis/tasks.py | 14 + apps/analysis/urls.py | 18 + apps/analysis/views.py | 180 ++++++- apps/aws/urls.py | 24 +- apps/aws/utils.py | 111 +++-- apps/aws/views.py | 286 ++++++++++- apps/data/admin.py | 5 +- apps/data/migrations/0005_detectionresult.py | 30 ++ ...006_alter_detectionresult_case_and_more.py | 31 ++ apps/data/models.py | 16 + apps/data/urls.py | 5 +- apps/data/views.py | 128 ++++- scope/urls.py | 1 + static/images/cloud/search.svg | 1 + static/images/logo/icon.png | Bin 69860 -> 0 bytes static/images/logo/icon.svg | 9 - static/images/logo/logo-mobile.svg | 9 + static/images/logo/logo.png | Bin 18521 -> 0 bytes static/images/logo/logo.svg | 6 +- templates/analysis/case_detections.html | 187 +++++++ .../analysis/detection_confirm_delete.html | 38 ++ templates/analysis/detection_form.html | 131 +++++ templates/analysis/detection_list.html | 83 +++ templates/aws/account_resources.html | 471 +++++++++++++----- templates/aws/connect_aws.html | 57 ++- templates/aws/credential_details.html | 268 +++++----- templates/aws/edit_account.html | 58 ++- templates/aws/fetch_cloudtrail_logs.html | 337 +++++++++---- templates/aws/get_logs.html | 195 ++++---- templates/aws/logsource_details.html | 92 ++-- templates/aws/resource_details.html | 88 ++-- templates/case/case_detail.html | 203 +++++--- templates/case/connect_client.html | 41 +- templates/case/create_case.html | 73 ++- templates/case/edit_case.html | 60 ++- templates/data/normalized_logs.html | 273 +++++++--- templates/web/app/app_base.html | 2 +- templates/web/app_home.html | 93 ++-- templates/web/components/hero.html | 21 +- templates/web/components/top_nav.html | 2 +- 47 files changed, 3329 insertions(+), 862 deletions(-) create mode 100644 README_NEW.md create mode 100644 apps/analysis/detection_rules/aws_rules.yaml create mode 100644 apps/analysis/detections.py create mode 100644 apps/analysis/forms.py create mode 100644 apps/analysis/management/commands/load_detection_rules.py create mode 100644 apps/analysis/migrations/0001_initial.py create mode 100644 apps/analysis/tasks.py create mode 100644 apps/analysis/urls.py create mode 100644 apps/data/migrations/0005_detectionresult.py create mode 100644 apps/data/migrations/0006_alter_detectionresult_case_and_more.py create mode 100644 static/images/cloud/search.svg delete mode 100644 static/images/logo/icon.png delete mode 100644 static/images/logo/icon.svg create mode 100644 static/images/logo/logo-mobile.svg delete mode 100644 static/images/logo/logo.png create mode 100644 templates/analysis/case_detections.html create mode 100644 templates/analysis/detection_confirm_delete.html create mode 100644 templates/analysis/detection_form.html create mode 100644 templates/analysis/detection_list.html diff --git a/README_NEW.md b/README_NEW.md new file mode 100644 index 0000000..ac79806 --- /dev/null +++ b/README_NEW.md @@ -0,0 +1,247 @@ + + +[![Contributors][contributors-shield]][contributors-url] +[![Forks][forks-shield]][forks-url] +[![Stargazers][stars-shield]][stars-url] +[![Issues][issues-shield]][issues-url] +[![Apache-2.0 license +][license-shield]][license-url] +[![LinkedIn][linkedin-shield]][linkedin-url] + + + + +
+
+ + Logo + + +

Scope

+ +

+ Scope is an open source cloud forensic tools to allow for rapid incident responce in Amazon Web Services (AWS). Support for Google Cloud Platform (GCP) and Microsoft Azure (Azure) is comming soon. + + +
+ Explore the docs ยป +
+
+ View Demo + · + Report Bug + · + Request Feature +

+
+ + + + +
+ Table of Contents +
    +
  1. + About The Project + +
  2. +
  3. + Getting Started + +
  4. +
  5. Usage
  6. +
  7. Roadmap
  8. +
  9. Contributing
  10. +
  11. License
  12. +
  13. Contact
  14. +
  15. Acknowledgments
  16. +
+
+ + + + +## About The Project + +[![Product Name Screen Shot][product-screenshot]](https://example.com) + +

(back to top)

+ + + +### Built With + +* [![Python][python.org]][Python-url] +* [![Django][Djangoproject.com]][Django-url] +* [![Bootstrap][Bootstrap.com]][Bootstrap-url] +* [![HTMX][Htmx.org]][Htmx-url] +* [![Docker][Docker.com]][Docker-url] + +

(back to top)

+ + + + +## Getting Started + +This is an example of how you may give instructions on setting up your project locally. +To get a local copy up and running follow these simple example steps. + +### Prerequisites + +This is an example of how to list things you need to use the software and how to install them. +* npm + ```sh + npm install npm@latest -g + ``` + +### Installation + +1. Get a free API Key at [https://example.com](https://example.com) +2. Clone the repo + ```sh + git clone https://github.com/scope-forensics/scope.git + ``` +3. Install NPM packages + ```sh + npm install + ``` +4. Enter your API in `config.js` + ```js + const API_KEY = 'ENTER YOUR API'; + ``` +5. Change git remote url to avoid accidental pushes to base project + ```sh + git remote set-url origin scope-forensics/scope + git remote -v # confirm the changes + ``` + +

(back to top)

+ + + + +## Usage + +Use this space to show useful examples of how a project can be used. Additional screenshots, code examples and demos work well in this space. You may also link to more resources. + +_For more examples, please refer to the [Documentation](https://example.com)_ + +

(back to top)

+ + + + +## Roadmap + +- [ ] AWS +- [ ] Azure +- [ ] GCP +- [ ] Feature 3 + - [ ] Nested Feature + +See the [open issues](https://github.com/scope-forensics/scope/issues) for a full list of proposed features (and known issues). + +

(back to top)

+ + + + +## Contributing + +Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". +Don't forget to give the project a star! Thanks again! + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +

(back to top)

+ +### Top contributors: + + + contrib.rocks image + + + + + +## License + +Distributed under the Apache-2.0 license +. See `LICENSE.txt` for more information. + +

(back to top)

+ + + + +## Contact + +Your Name - [@twitter_handle](https://twitter.com/twitter_handle) - scopeforenscis@protonmail.com.com + +Project Link: [https://github.com/scope-forensics/scope](https://github.com/scope-forensics/scope) + +

(back to top)

+ + + + +## Acknowledgments + +* []() +* []() +* []() + +

(back to top)

+ + + + + +[contributors-shield]: https://img.shields.io/github/contributors/scope-forensics/scope.svg?style=for-the-badge +[contributors-url]: https://github.com/scope-forensics/scope/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/scope-forensics/scope.svg?style=for-the-badge +[forks-url]: https://github.com/scope-forensics/scope/network/members +[stars-shield]: https://img.shields.io/github/stars/scope-forensics/scope.svg?style=for-the-badge +[stars-url]: https://github.com/scope-forensics/scope/stargazers +[issues-shield]: https://img.shields.io/github/issues/scope-forensics/scope.svg?style=for-the-badge +[issues-url]: https://github.com/scope-forensics/scope/issues +[license-shield]: https://img.shields.io/github/license/scope-forensics/scope.svg?style=for-the-badge +[license-url]: https://github.com/scope-forensics/scope/blob/master/LICENSE.txt +[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 +[linkedin-url]: https://linkedin.com/in/linkedin_username +[product-screenshot]: images/screenshot.png +[Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white +[Next-url]: https://nextjs.org/ +[React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB +[React-url]: https://reactjs.org/ +[Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs&logoColor=4FC08D +[Vue-url]: https://vuejs.org/ +[Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white +[Angular-url]: https://angular.io/ +[Svelte.dev]: https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00 +[Svelte-url]: https://svelte.dev/ +[Laravel.com]: https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white +[Laravel-url]: https://laravel.com +[Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white +[Bootstrap-url]: https://getbootstrap.com +[Htmx.org]: https://img.shields.io/badge/Htmx-563D7C?style=for-the-badge&logo=htmx&logoColor=white +[Htmx-url]: https://htmx.org +[Docker.com]: https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white +[Docker-url]: https://docker.com +[Djangoproject.com]: https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=white +[Django-url]: https://djangoproject.com +[Python.org]: https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white +[Python-url]: https://python.org + diff --git a/apps/analysis/detection_rules/aws_rules.yaml b/apps/analysis/detection_rules/aws_rules.yaml new file mode 100644 index 0000000..412819f --- /dev/null +++ b/apps/analysis/detection_rules/aws_rules.yaml @@ -0,0 +1,41 @@ +# AWS Pre-built Detection Rules + +- name: "GetCallerIdentity Reconnaissance" + description: "Detects attempts to enumerate AWS account information using GetCallerIdentity" + cloud: "aws" + detection_type: "api_call" + severity: "medium" + event_source: "sts.amazonaws.com" + event_name: "GetCallerIdentity" + auto_tags: ["suspicious", "reconnaissance"] + enabled: true + +- name: "Root Account Usage" + description: "Detects usage of the root account which is a security best practice violation" + cloud: "aws" + detection_type: "login" + severity: "high" + event_source: "signin.amazonaws.com" + additional_criteria: {"user_identity": "root"} + auto_tags: ["high-risk", "compliance-violation"] + enabled: true + +- name: "Security Group Modification" + description: "Detects modifications to security groups which could indicate network security changes" + cloud: "aws" + detection_type: "network" + severity: "medium" + event_source: "ec2.amazonaws.com" + event_name: "AuthorizeSecurityGroupIngress" + auto_tags: ["security-group-change", "network-modification"] + enabled: true + +- name: "IAM Policy Changes" + description: "Detects changes to IAM policies which could indicate privilege escalation attempts" + cloud: "aws" + detection_type: "iam" + severity: "high" + event_source: "iam.amazonaws.com" + event_name: "PutRolePolicy" + auto_tags: ["iam-change", "privilege-escalation"] + enabled: true \ No newline at end of file diff --git a/apps/analysis/detections.py b/apps/analysis/detections.py new file mode 100644 index 0000000..b37693f --- /dev/null +++ b/apps/analysis/detections.py @@ -0,0 +1,87 @@ +from django.db.models import Q +from apps.data.models import NormalizedLog, DetectionResult +from apps.analysis.models import Detection + +def get_case_logs(case_id): + """Get all logs for a case""" + logs = NormalizedLog.objects.filter(case_id=case_id) + print(f"\nFound {logs.count()} logs for case {case_id}") + return logs + +def apply_detection_filters(logs, detection): + """Apply detection rule filters to logs""" + print(f"\nApplying filters for detection: {detection.name}") + + # Apply event name filter + if detection.event_name: + print(f"Filtering for event_name: {detection.event_name}") + logs = logs.filter(event_name__iexact=detection.event_name) + print(f"Found {logs.count()} logs with matching event name") + # Debug: show matching logs + for log in logs: + print(f"Matching log - Event: {log.event_name}, Source: {log.event_source}") + + # Apply event source filter + if detection.event_source: + print(f"Filtering for event_source: {detection.event_source}") + logs = logs.filter(event_source__iexact=detection.event_source) + print(f"Found {logs.count()} logs with matching event source") + + # Apply event type filter + if detection.event_type: + print(f"Filtering for event_type: {detection.event_type}") + logs = logs.filter(event_type__iexact=detection.event_type) + print(f"Found {logs.count()} logs with matching event type") + + # Apply additional criteria + if detection.additional_criteria: + for key, value in detection.additional_criteria.items(): + if key == 'raw_data_contains': + logs = logs.filter(raw_data__icontains=value) + elif key == 'ip_address': + logs = logs.filter(ip_address=value) + elif key == 'user_identity': + logs = logs.filter(user_identity=value) + + return logs + +def tag_matching_logs(logs, detection): + """Add detection tags to matching logs""" + for log in logs: + log.tags.add(*detection.auto_tags.all()) + +def run_detection(case_id, account_id, detection): + """Run a single detection rule""" + # Get base logs + logs = get_case_logs(case_id) + + # Apply detection filters + matching_logs = apply_detection_filters(logs, detection) + + # Create detection results and tag logs + for log in matching_logs: + # Create detection result if it doesn't exist + DetectionResult.objects.get_or_create( + case_id=case_id, + detection=detection, + matched_log=log + ) + # Tag the log + log.tags.add(*detection.auto_tags.all()) + + return matching_logs + +def run_all_detections(case_id, account_id): + """Run all enabled AWS detections""" + results = [] + detections = Detection.objects.filter(enabled=True, cloud='aws') + + for detection in detections: + matching_logs = run_detection(case_id, account_id, detection) + results.append({ + 'detection': detection, + 'matches': matching_logs.count(), + 'matching_logs': matching_logs + }) + + return results diff --git a/apps/analysis/forms.py b/apps/analysis/forms.py new file mode 100644 index 0000000..fbd42a8 --- /dev/null +++ b/apps/analysis/forms.py @@ -0,0 +1,21 @@ +from django import forms +from .models import Detection + +class DetectionForm(forms.ModelForm): + class Meta: + model = Detection + fields = ['name', 'description', 'cloud', 'detection_type', 'severity', + 'event_source', 'event_name', 'event_type', 'additional_criteria', + 'auto_tags', 'enabled'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Add Bootstrap classes to all fields + for field in self.fields.values(): + if isinstance(field.widget, forms.TextInput) or \ + isinstance(field.widget, forms.Select) or \ + isinstance(field.widget, forms.Textarea): + field.widget.attrs.update({'class': 'form-control'}) + elif isinstance(field.widget, forms.CheckboxInput): + field.widget.attrs.update({'class': 'form-check-input'}) + self.fields['additional_criteria'].widget = forms.Textarea(attrs={'rows': 4}) \ No newline at end of file diff --git a/apps/analysis/management/commands/load_detection_rules.py b/apps/analysis/management/commands/load_detection_rules.py new file mode 100644 index 0000000..6d61920 --- /dev/null +++ b/apps/analysis/management/commands/load_detection_rules.py @@ -0,0 +1,66 @@ +from django.core.management.base import BaseCommand +from django.db import transaction +from apps.analysis.models import Detection +from apps.data.models import Tag +import yaml +import os +from pathlib import Path + +class Command(BaseCommand): + help = 'Load pre-built detection rules from YAML files' + + def add_arguments(self, parser): + parser.add_argument( + '--force', + action='store_true', + help='Force reload all rules, overwriting existing ones', + ) + + def handle(self, *args, **options): + rules_dir = Path(__file__).resolve().parent.parent.parent / 'detection_rules' + force = options['force'] + + for yaml_file in rules_dir.glob('*.yaml'): + self.stdout.write(f'Processing {yaml_file.name}...') + + with open(yaml_file) as f: + rules = yaml.safe_load(f) + + with transaction.atomic(): + for rule in rules: + # Handle auto_tags + auto_tags = rule.pop('auto_tags', []) + + # Create or get the detection rule + detection, created = Detection.objects.update_or_create( + name=rule['name'], + defaults=rule + ) + + if created: + self.stdout.write(self.style.SUCCESS( + f'Created detection rule: {detection.name}' + )) + elif force: + self.stdout.write(self.style.WARNING( + f'Updated existing detection rule: {detection.name}' + )) + else: + self.stdout.write(self.style.NOTICE( + f'Skipped existing detection rule: {detection.name}' + )) + + # Handle tags + if created or force: + # Clear existing tags if updating + detection.auto_tags.clear() + + # Create and add tags + for tag_name in auto_tags: + tag, _ = Tag.objects.get_or_create( + name=tag_name.title(), + slug=tag_name.lower().replace(' ', '-') + ) + detection.auto_tags.add(tag) + + self.stdout.write(self.style.SUCCESS('Successfully loaded detection rules')) \ No newline at end of file diff --git a/apps/analysis/migrations/0001_initial.py b/apps/analysis/migrations/0001_initial.py new file mode 100644 index 0000000..5f39d0b --- /dev/null +++ b/apps/analysis/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 5.1.3 on 2025-02-13 23:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('data', '0004_alter_normalizedlog_unique_together'), + ] + + operations = [ + migrations.CreateModel( + name='Detection', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('description', models.TextField()), + ('cloud', models.CharField(choices=[('aws', 'Amazon Web Services'), ('gcp', 'Google Cloud Platform'), ('azure', 'Microsoft Azure')], max_length=10)), + ('detection_type', models.CharField(choices=[('api_call', 'API Call'), ('login', 'Login Activity'), ('data_access', 'Data Access'), ('network', 'Network Activity'), ('iam', 'IAM Changes'), ('other', 'Other')], max_length=20)), + ('enabled', models.BooleanField(default=True)), + ('severity', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], default='medium', max_length=10)), + ('event_source', models.CharField(blank=True, max_length=1000, null=True)), + ('event_name', models.CharField(blank=True, max_length=1000, null=True)), + ('event_type', models.CharField(blank=True, max_length=1000, null=True)), + ('additional_criteria', models.JSONField(blank=True, default=dict)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('auto_tags', models.ManyToManyField(blank=True, to='data.tag')), + ], + ), + ] diff --git a/apps/analysis/models.py b/apps/analysis/models.py index 71a8362..74b36b0 100644 --- a/apps/analysis/models.py +++ b/apps/analysis/models.py @@ -1,3 +1,49 @@ from django.db import models +from apps.data.models import Tag -# Create your models here. +class Detection(models.Model): + CLOUD_CHOICES = [ + ('aws', 'Amazon Web Services'), + ('gcp', 'Google Cloud Platform'), + ('azure', 'Microsoft Azure'), + ] + + DETECTION_TYPE = [ + ('api_call', 'API Call'), + ('login', 'Login Activity'), + ('data_access', 'Data Access'), + ('network', 'Network Activity'), + ('iam', 'IAM Changes'), + ('other', 'Other') + ] + + SEVERITY_CHOICES = [ + ('low', 'Low'), + ('medium', 'Medium'), + ('high', 'High'), + ('critical', 'Critical') + ] + + name = models.CharField(max_length=200) + description = models.TextField() + cloud = models.CharField(max_length=10, choices=CLOUD_CHOICES) + detection_type = models.CharField(max_length=20, choices=DETECTION_TYPE) + enabled = models.BooleanField(default=True) + severity = models.CharField(max_length=10, choices=SEVERITY_CHOICES, default='medium') + + # Detection criteria + event_source = models.CharField(max_length=1000, blank=True, null=True) + event_name = models.CharField(max_length=1000, blank=True, null=True) + event_type = models.CharField(max_length=1000, blank=True, null=True) + + # Additional filters as JSON + additional_criteria = models.JSONField(default=dict, blank=True) + + # Auto-tag matches with these tags + auto_tags = models.ManyToManyField(Tag, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.name} ({self.cloud})" \ No newline at end of file diff --git a/apps/analysis/tasks.py b/apps/analysis/tasks.py new file mode 100644 index 0000000..765a51e --- /dev/null +++ b/apps/analysis/tasks.py @@ -0,0 +1,14 @@ +from celery import shared_task +from .detections import run_all_detections + +@shared_task +def run_detections_task(case_id, account_id): + """Celery task to run detections""" + results = run_all_detections(case_id, account_id) + + total_matches = sum(r['matches'] for r in results) + + return { + 'total_detections': len(results), + 'total_matches': total_matches + } diff --git a/apps/analysis/urls.py b/apps/analysis/urls.py new file mode 100644 index 0000000..8a9a023 --- /dev/null +++ b/apps/analysis/urls.py @@ -0,0 +1,18 @@ +from django.urls import path +from . import views + +app_name = 'analysis' + +urlpatterns = [ + path('case//detections/', views.case_detections, name='case_detections'), + path('case//detections/rules/', views.detection_list, name='detection_list'), + path('case//detections/rules/create/', views.detection_create, name='detection_create'), + path('case//detections/rules//edit/', views.detection_edit, name='detection_edit'), + path('case//detections/rules//delete/', views.detection_delete, name='detection_delete'), + path('case//detections/run/', views.run_detections, name='run_detections'), + path('case//detections/rules/load-prebuilt/', views.load_prebuilt_rules, name='load_prebuilt_rules'), + path('case//detection-result//tag/', + views.tag_detection_result, name='tag_detection_result'), + path('api/detection-result//tags/', + views.get_detection_result_tags, name='get_detection_result_tags'), +] \ No newline at end of file diff --git a/apps/analysis/views.py b/apps/analysis/views.py index 91ea44a..6e61046 100644 --- a/apps/analysis/views.py +++ b/apps/analysis/views.py @@ -1,3 +1,179 @@ -from django.shortcuts import render +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.http import JsonResponse +from .models import Detection +from apps.data.models import DetectionResult, NormalizedLog +from .forms import DetectionForm +from .tasks import run_detections_task +from apps.case.models import Case +from django.core.management import call_command +from io import StringIO +from apps.aws.models import AWSAccount +from .models import Tag -# Create your views here. +@login_required +def detection_list(request, case_id): + case = get_object_or_404(Case, id=case_id) + detections = Detection.objects.all() + return render(request, 'analysis/detection_list.html', { + 'detections': detections, + 'case': case + }) + +@login_required +def detection_create(request, case_id): + case = get_object_or_404(Case, id=case_id) + if request.method == 'POST': + form = DetectionForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, 'Detection rule created successfully.') + return redirect('analysis:detection_list', case_id=case_id) + else: + form = DetectionForm() + + return render(request, 'analysis/detection_form.html', { + 'form': form, + 'is_create': True, + 'case': case + }) + +@login_required +def detection_edit(request, case_id, pk): + case = get_object_or_404(Case, id=case_id) + detection = get_object_or_404(Detection, pk=pk) + + if request.method == 'POST': + form = DetectionForm(request.POST, instance=detection) + if form.is_valid(): + form.save() + messages.success(request, 'Detection rule updated successfully.') + return redirect('analysis:detection_list', case_id=case_id) + else: + form = DetectionForm(instance=detection) + + return render(request, 'analysis/detection_form.html', { + 'form': form, + 'is_create': False, + 'detection': detection, + 'case': case + }) + +@login_required +def detection_delete(request, case_id, pk): + case = get_object_or_404(Case, id=case_id) + detection = get_object_or_404(Detection, pk=pk) + + if request.method == 'POST': + detection.delete() + messages.success(request, 'Detection rule deleted successfully.') + return redirect('analysis:detection_list', case_id=case_id) + + return render(request, 'analysis/detection_confirm_delete.html', { + 'detection': detection, + 'case': case + }) + +@login_required +def run_detections(request, case_id): + """Trigger detection run for a case""" + if request.method == 'POST': + # Debug: Check if the log exists at all + print("\nChecking for GetCallerIdentity logs:") + all_logs = NormalizedLog.objects.filter( + event_name='GetCallerIdentity' + ) + print(f"Found {all_logs.count()} total GetCallerIdentity logs") + for log in all_logs: + print(f"Case ID: {log.case_id}") + print(f"AWS Account ID: {log.aws_account_id}") + print(f"Event source: {log.event_source}") + print(f"Event name: {log.event_name}") + print("---") + + # Get the AWS account for this case + aws_account = AWSAccount.objects.filter(case_id=case_id).first() + if not aws_account: + messages.error(request, 'No AWS account found for this case') + return redirect('analysis:case_detections', case_id=case_id) + + task = run_detections_task.delay(case_id, aws_account.account_id) + messages.success(request, 'Detection scan started. Results will be available shortly.') + return redirect('analysis:case_detections', case_id=case_id) + return redirect('analysis:case_detections', case_id=case_id) + +@login_required +def detection_results(request, case_id): + results = DetectionResult.objects.filter( + case_id=case_id + ).select_related('detection', 'matched_log') + + return render(request, 'analysis/detection_results.html', { + 'results': results + }) + +@login_required +def case_detections(request, case_id): + """Main detections page showing results and management options""" + case = get_object_or_404(Case, id=case_id) + detection_results = DetectionResult.objects.filter( + case_id=case_id + ).select_related('detection', 'matched_log').order_by('-created_at') + + # Get all available tags + available_tags = Tag.objects.all() + + # Group results by detection + results_by_detection = {} + for result in detection_results: + if result.detection not in results_by_detection: + results_by_detection[result.detection] = [] + results_by_detection[result.detection].append(result) + + context = { + 'case': case, + 'results_by_detection': results_by_detection, + 'total_results': detection_results.count(), + 'detection_count': Detection.objects.filter(enabled=True).count(), + 'available_tags': available_tags + } + + return render(request, 'analysis/case_detections.html', context) + +@login_required +def load_prebuilt_rules(request, case_id): + if request.method == 'POST': + try: + # Capture command output + out = StringIO() + call_command('load_detection_rules', stdout=out) + messages.success(request, 'Pre-built detection rules loaded successfully') + return redirect('analysis:detection_list', case_id=case_id) + except Exception as e: + messages.error(request, f'Error loading pre-built rules: {str(e)}') + return redirect('analysis:detection_list', case_id=case_id) + return redirect('analysis:detection_list', case_id=case_id) + +@login_required +def tag_detection_result(request, case_id, result_id): + if request.method == 'POST': + result = get_object_or_404(DetectionResult, id=result_id, case_id=case_id) + tag_ids = request.POST.getlist('tag_ids') + + # Clear existing tags + result.matched_log.tags.clear() + + # Add selected tags + if tag_ids: + tags = Tag.objects.filter(id__in=tag_ids) + result.matched_log.tags.add(*tags) + messages.success(request, 'Tags updated successfully') + + return redirect('analysis:case_detections', case_id=case_id) + +@login_required +def get_detection_result_tags(request, result_id): + result = get_object_or_404(DetectionResult, id=result_id) + tags = list(result.matched_log.tags.values_list('id', flat=True)) + return JsonResponse({'tags': tags}) diff --git a/apps/aws/urls.py b/apps/aws/urls.py index 656d11b..7c40b04 100644 --- a/apps/aws/urls.py +++ b/apps/aws/urls.py @@ -7,16 +7,26 @@ urlpatterns = [ path('/connect/aws/', views.connect_aws, name='connect_aws'), - path('accounts//edit/', views.edit_account, name='edit_account'), - path('accounts//delete/', views.delete_account, name='delete_account'), - path('accounts//pull-resources/', views.pull_resources_view, name='pull_aws_resources'), + path('accounts//edit/', views.edit_account, name='edit_account'), + path('accounts//delete/', views.delete_account, name='delete_account'), + path('accounts//pull-resources/', views.pull_resources_view, name='pull_aws_resources'), path('resources//details/', views.aws_resource_details, name='aws_resource_details'), - path('accounts//account-resources/', views.account_resources, name='account_resources'), + path('accounts//account-resources/', views.account_resources, name='account_resources'), path('logsource///details/', views.aws_logsource_details, name='aws_logsource_details'), - path('fetch-management-events//', views.trigger_management_event_fetch, name='fetch_management_events'), + path('fetch-management-events//', views.trigger_management_event_fetch, name='fetch_management_events'), path('browse-s3-structure/', views.browse_s3_structure, name='browse_s3_structure'), - path('fetch-logs//', views.fetch_cloudtrail_logs, name='fetch_cloudtrail_logs'), - path('accounts//logs/', views.normalized_logs_view, name='normalized_logs'), + path('fetch-logs//', views.fetch_cloudtrail_logs, name='fetch_cloudtrail_logs'), + path('accounts//logs/', views.normalized_logs_view, name='normalized_logs'), path('credential//', views.aws_credential_details, name='aws_credential_details'), + path('resources//add-tag/', views.add_tag_to_resource, name='add_tag_to_resource'), + path('credentials//add-tag/', views.add_tag_to_credential, name='add_tag_to_credential'), + path('logsources//add-tag/', views.add_tag_to_logsource, name='add_tag_to_logsource'), + path('resources//edit-tag//', views.edit_resource_tag, name='edit_resource_tag'), + path('credentials//edit-tag//', views.edit_credential_tag, name='edit_credential_tag'), + path('logsources//edit-tag//', views.edit_logsource_tag, name='edit_logsource_tag'), + path('resources//remove-tag//', views.remove_tag_from_resource, name='remove_tag_from_resource'), + path('credentials//remove-tag//', views.remove_tag_from_credential, name='remove_tag_from_credential'), + path('logsources//remove-tag//', views.remove_tag_from_logsource, name='remove_tag_from_logsource'), + path('suggest-cloudtrail-prefix/', views.suggest_cloudtrail_prefix, name='suggest_cloudtrail_prefix'), ] diff --git a/apps/aws/utils.py b/apps/aws/utils.py index 839f789..9f2b644 100644 --- a/apps/aws/utils.py +++ b/apps/aws/utils.py @@ -65,27 +65,28 @@ def fetch_credential_report(): try: iam = session.client('iam') - # Generate credential report - this may take a few seconds + # Generate credential report response = iam.generate_credential_report() - # Wait for report to be generated - state = response.get('State', '') - while state != 'COMPLETE': - logger.info(f"Waiting for credential report to be generated... Current state: {state}") - time.sleep(2) + # Wait for report to be generated with timeout + max_attempts = 10 # Maximum number of attempts + attempt = 0 + while attempt < max_attempts: try: response = iam.get_credential_report() - state = response.get('State', '') + if response.get('Content'): + break except iam.exceptions.CredentialReportNotPresentException: - logger.info("Report not ready yet, retrying...") + logger.info(f"Report not ready yet, attempt {attempt + 1}/{max_attempts}") + time.sleep(2) + attempt += 1 continue - # Get the credential report - response = iam.get_credential_report() - if 'Content' not in response: - logger.error("No content in credential report response") + if attempt >= max_attempts: + logger.error("Timed out waiting for credential report") return + # Get and process the credential report report_csv = response['Content'].decode('utf-8') # Parse CSV content @@ -524,47 +525,71 @@ def fetch_and_normalize_cloudtrail_logs(account_id, resource_id, prefix, start_d prefix += "/" current_date = start_date_obj - with transaction.atomic(): - while current_date <= end_date_obj: - date_folder = f"{current_date.year}/{current_date.strftime('%m')}/{current_date.strftime('%d')}/" - final_prefix = prefix + date_folder + while current_date <= end_date_obj: + date_folder = f"{current_date.year}/{current_date.strftime('%m')}/{current_date.strftime('%d')}/" + final_prefix = prefix + date_folder if prefix else date_folder - logger.info(f"Checking prefix '{final_prefix}' in bucket '{bucket_name}'") + logger.info(f"Checking prefix '{final_prefix}' in bucket '{bucket_name}'") - try: - paginator = s3.get_paginator("list_objects_v2") - for page in paginator.paginate(Bucket=bucket_name, Prefix=final_prefix): - for obj in page.get("Contents", []): - key = obj["Key"] - try: - resp = s3.get_object(Bucket=bucket_name, Key=key) - raw_body = resp["Body"].read() - - # Decompress if gzipped - if key.endswith(".gz"): - file_data = gzip.decompress(raw_body).decode("utf-8") - else: - file_data = raw_body.decode("utf-8") - - records = json.loads(file_data).get("Records", []) + try: + paginator = s3.get_paginator("list_objects_v2") + for page in paginator.paginate(Bucket=bucket_name, Prefix=final_prefix): + for obj in page.get("Contents", []): + key = obj["Key"] + try: + resp = s3.get_object(Bucket=bucket_name, Key=key) + raw_body = resp["Body"].read() + + # Decompress if gzipped + if key.endswith(".gz"): + file_data = gzip.decompress(raw_body).decode("utf-8") + else: + file_data = raw_body.decode("utf-8") + + json_data = json.loads(file_data) + records = json_data.get("Records", []) + + # Process records in smaller batches + batch_size = 100 + for i in range(0, len(records), batch_size): + batch_records = records[i:i + batch_size] + normalized_logs = [] - # Process each record - for record in records: + # Prepare batch of normalized logs + for record in batch_records: try: normalized_data = normalize_cloudtrail_event(record, case, aws_account) - NormalizedLog.objects.create(**normalized_data) + if normalized_data: + normalized_logs.append(NormalizedLog(**normalized_data)) except Exception as e: - logger.error(f"Error processing record: {e}") + logger.error(f"Error normalizing record: {e}") continue + + # Bulk create the batch in a transaction + if normalized_logs: + try: + with transaction.atomic(): + NormalizedLog.objects.bulk_create(normalized_logs, batch_size=batch_size) + except Exception as e: + logger.error(f"Error bulk creating logs: {e}") + # If bulk create fails, try individual creates + for log in normalized_logs: + try: + with transaction.atomic(): + log.save() + except Exception as individual_error: + logger.error(f"Error saving individual log: {individual_error}") + + except Exception as e: + logger.error(f"Error processing file {key}: {e}") + continue - except Exception as e: - logger.error(f"Error processing file {key}: {e}") - continue + except Exception as e: + logger.error(f"Error processing date {current_date}: {e}") - except Exception as e: - logger.error(f"Error processing date {current_date}: {e}") + current_date += timedelta(days=1) - current_date += timedelta(days=1) + logger.info("Completed processing CloudTrail logs") def fetch_management_event_history(account_id, case_id): """Fetch and normalize CloudTrail management events using LookupEvents API""" diff --git a/apps/aws/views.py b/apps/aws/views.py index c5e2f65..6fc3c43 100644 --- a/apps/aws/views.py +++ b/apps/aws/views.py @@ -1,7 +1,7 @@ import boto3 from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required -from .models import AWSAccount, AWSResource, AWSLogSource, AWSCredential +from .models import AWSAccount, AWSResource, AWSLogSource, AWSCredential, Tag from .forms import AWSAccountForm, FetchCloudTrailLogsForm from apps.case.models import Case from .utils import validate_aws_credentials @@ -58,7 +58,7 @@ def connect_aws(request, slug): # Edit aws connection @login_required def edit_account(request, account_id): - account = get_object_or_404(AWSAccount, id=account_id) + account = get_object_or_404(AWSAccount, account_id=account_id) if request.method == "POST": form = AWSAccountForm(request.POST, instance=account) if form.is_valid(): @@ -101,7 +101,14 @@ def aws_resource_details(request, resource_id): Fetch and return details for a specific AWS resource. """ resource = get_object_or_404(AWSResource, id=resource_id) - return render(request, 'aws/resource_details.html', {'resource': resource}) + account = resource.account + case = account.case + + return render(request, 'aws/resource_details.html', { + 'resource': resource, + 'account': account, + 'case': case + }) # this renders both the aws resources and logging sources into one page @login_required @@ -130,6 +137,9 @@ def account_resources(request, account_id): aws_credentials = AWSCredential.objects.filter(account=aws_account) + # Get all available tags + all_tags = Tag.objects.all() + context = { 'aws_account': aws_account, 'case': case, @@ -137,10 +147,11 @@ def account_resources(request, account_id): 'grouped_log_sources': grouped_log_sources, 'error_messages': error_messages, 'aws_credentials': aws_credentials, + 'all_tags': all_tags, } return render(request, 'aws/account_resources.html', context) - +# The main view to display the overview of the logs for a specific AWS account. @login_required def normalized_logs_view(request, account_id): aws_account = get_object_or_404(AWSAccount, account_id=account_id) @@ -181,17 +192,23 @@ def normalized_logs_view(request, account_id): } return render(request, "aws/get_logs.html", context) +# Fetch and return details for a specific AWS log source using its slug. @login_required def aws_logsource_details(request, slug): """ - Fetch and return details for a specific AWS log source using its slug. + Fetch and return details for a specific AWS log source. """ log_source = get_object_or_404(AWSLogSource, slug=slug) - + account = log_source.account # Get the AWS account + case = account.case + context = { 'log_source': log_source, + 'account': account, + 'case': case, + 'aws_account': account # Add this for consistency with other templates } - + return render(request, 'aws/logsource_details.html', context) # This allows a user to pull CloudTrail logs that are stored in an s3 bucket. @@ -205,20 +222,84 @@ def browse_s3_structure(request): session = boto3.Session( aws_access_key_id=account.aws_access_key, aws_secret_access_key=account.aws_secret_key, - region_name=resource.aws_region or account.default_region) + region_name=resource.aws_region or account.aws_region) s3 = session.client("s3") bucket_name = resource.resource_name or resource.resource_id if current_prefix and not current_prefix.endswith("/"): current_prefix += "/" - paginator = s3.get_paginator("list_objects_v2") - subfolders = [] + paginator = s3.get_paginator("list_objects_v2") + subfolders = [] + files = [] + for page in paginator.paginate(Bucket=bucket_name, Prefix=current_prefix, Delimiter="/"): + # Get subfolders for cp in page.get("CommonPrefixes", []): subfolders.append(cp["Prefix"]) - - return JsonResponse({"subfolders": subfolders}) + + # Get files + for obj in page.get("Contents", []): + if not obj["Key"].endswith("/"): # Skip folder objects + files.append({ + "key": obj["Key"], + "size": obj["Size"], + "last_modified": obj["LastModified"].isoformat() + }) + + return JsonResponse({ + "subfolders": subfolders, + "files": files + }) + +# Helper function to suggest CloudTrail prefixes based on date range +@login_required +def suggest_cloudtrail_prefix(request): + """Endpoint to automatically suggest CloudTrail prefixes based on date range""" + resource_id = request.GET.get("resource_id") + start_date = request.GET.get("start_date") + end_date = request.GET.get("end_date") + + if not all([resource_id, start_date, end_date]): + return JsonResponse({"error": "Missing required parameters"}, status=400) + + try: + resource = get_object_or_404(AWSResource, id=resource_id) + account = resource.account + + # Use resource region or fall back to account's default region + region = resource.aws_region or account.aws_region + if not region: + return JsonResponse({"error": "No region specified for resource or account"}, status=400) + + # Convert dates to datetime objects + start_date = datetime.strptime(start_date, "%Y-%m-%d") + end_date = datetime.strptime(end_date, "%Y-%m-%d") + + # Generate list of possible prefixes based on date range + prefixes = [] + current_date = start_date + while current_date <= end_date: + # Standard CloudTrail log path structure: + # AWSLogs/aws-account-id/CloudTrail/region/YYYY/MM/DD/ + prefix = (f"AWSLogs/{account.account_id}/CloudTrail/{region}/" + f"{current_date.strftime('%Y/%m/%d/')}") + prefixes.append(prefix) + current_date += timedelta(days=1) + + # Also suggest the root CloudTrail directory + root_prefix = f"AWSLogs/{account.account_id}/CloudTrail/{region}/" + if root_prefix not in prefixes: + prefixes.insert(0, root_prefix) + + return JsonResponse({ + "prefixes": prefixes, + "account_id": account.account_id, + "region": region + }) + except Exception as e: + logger.error(f"Error generating CloudTrail prefixes: {str(e)}") + return JsonResponse({"error": str(e)}, status=500) @login_required def fetch_cloudtrail_logs(request, account_id): @@ -235,20 +316,40 @@ def fetch_cloudtrail_logs(request, account_id): messages.error(request, "Selected bucket is not linked to this AWS account.") return redirect("case:case_detail", slug=resource.case.slug) + # If prefix doesn't start with the standard CloudTrail structure, construct it + if not prefix.startswith("AWSLogs/"): + region = resource.aws_region or aws_account.aws_region + if not region: + messages.error(request, "No region specified for resource or account") + return redirect("aws:fetch_cloudtrail_logs", account_id=account_id) + + # Construct the full CloudTrail path + base_prefix = f"AWSLogs/{aws_account.account_id}/CloudTrail/{region}/" + + # If prefix is just a date path (YYYY/MM/DD/), append it to base_prefix + if prefix.count('/') == 3: # Format: YYYY/MM/DD/ + prefix = base_prefix + prefix + else: + prefix = base_prefix + fetch_normalize_cloudtrail_logs_task.delay( account_id=aws_account.account_id, resource_id=resource.id, - prefix=prefix or "", + prefix=prefix, start_date=str(start_date), end_date=str(end_date), case_id=resource.case.id ) messages.success(request, "CloudTrail log fetching has been queued.") - return redirect("aws:get_logs", account_id=aws_account.account_id) + return redirect("aws:normalized_logs", account_id=aws_account.account_id) else: form = FetchCloudTrailLogsForm() - return render(request, "aws/fetch_cloudtrail_logs.html", {"form": form, "account_id": account_id}) + return render(request, "aws/fetch_cloudtrail_logs.html", { + "form": form, + "account_id": account_id, + "aws_account": aws_account + }) @login_required def trigger_management_event_fetch(request, account_id): @@ -260,7 +361,7 @@ def trigger_management_event_fetch(request, account_id): messages.success(request, "Management event history is being fetched.") logger.info(f"Task queued for AWS account {account_id}") - return redirect("aws:get_logs", account_id=aws_account.account_id) + return redirect("aws:normalized_logs", account_id=aws_account.account_id) @login_required def aws_credential_details(request, slug): @@ -277,3 +378,156 @@ def aws_credential_details(request, slug): return render(request, 'aws/credential_details.html', context) +@login_required +def add_tag_to_resource(request, resource_id): + if request.method == 'POST': + tag_id = request.POST.get('tag_id') + try: + resource = AWSResource.objects.get(id=resource_id) + tag = Tag.objects.get(id=tag_id) + resource.tags.add(tag) + messages.success(request, f'Tag "{tag.name}" added successfully.') + except (AWSResource.DoesNotExist, Tag.DoesNotExist): + messages.error(request, 'Error adding tag.') + return redirect('aws:account_resources', account_id=resource.account.account_id) + +@login_required +def add_tag_to_credential(request, credential_id): + if request.method == 'POST': + tag_id = request.POST.get('tag_id') + try: + credential = AWSCredential.objects.get(id=credential_id) + tag = Tag.objects.get(id=tag_id) + credential.tags.add(tag) + messages.success(request, f'Tag "{tag.name}" added successfully.') + except (AWSCredential.DoesNotExist, Tag.DoesNotExist): + messages.error(request, 'Error adding tag.') + return redirect('aws:account_resources', account_id=credential.account.account_id) + +@login_required +def add_tag_to_logsource(request, logsource_id): + if request.method == 'POST': + tag_id = request.POST.get('tag_id') + try: + logsource = AWSLogSource.objects.get(id=logsource_id) + tag = Tag.objects.get(id=tag_id) + logsource.tags.add(tag) + messages.success(request, f'Tag "{tag.name}" added successfully.') + except (AWSLogSource.DoesNotExist, Tag.DoesNotExist): + messages.error(request, 'Error adding tag.') + return redirect('aws:account_resources', account_id=logsource.account.account_id) + +@login_required +def edit_resource_tag(request, resource_id, tag_id): + if request.method == 'POST': + new_tag_id = request.POST.get('new_tag_id') + redirect_url = request.META.get('HTTP_REFERER', '') + + try: + resource = AWSResource.objects.get(id=resource_id) + old_tag = Tag.objects.get(id=tag_id) + new_tag = Tag.objects.get(id=new_tag_id) + + resource.tags.remove(old_tag) + resource.tags.add(new_tag) + messages.success(request, f'Tag updated from "{old_tag.name}" to "{new_tag.name}"') + except (AWSResource.DoesNotExist, Tag.DoesNotExist): + messages.error(request, 'Error updating tag.') + + if redirect_url: + return redirect(redirect_url) + return redirect('aws:account_resources', account_id=resource.account.account_id) + +@login_required +def edit_credential_tag(request, credential_id, tag_id): + if request.method == 'POST': + new_tag_id = request.POST.get('new_tag_id') + redirect_url = request.META.get('HTTP_REFERER', '') + + try: + credential = AWSCredential.objects.get(id=credential_id) + old_tag = Tag.objects.get(id=tag_id) + new_tag = Tag.objects.get(id=new_tag_id) + + credential.tags.remove(old_tag) + credential.tags.add(new_tag) + messages.success(request, f'Tag updated from "{old_tag.name}" to "{new_tag.name}"') + except (AWSCredential.DoesNotExist, Tag.DoesNotExist): + messages.error(request, 'Error updating tag.') + + if redirect_url: + return redirect(redirect_url) + return redirect('aws:account_resources', account_id=credential.account.account_id) + +@login_required +def edit_logsource_tag(request, logsource_id, tag_id): + if request.method == 'POST': + new_tag_id = request.POST.get('new_tag_id') + redirect_url = request.META.get('HTTP_REFERER', '') + + try: + logsource = AWSLogSource.objects.get(id=logsource_id) + old_tag = Tag.objects.get(id=tag_id) + new_tag = Tag.objects.get(id=new_tag_id) + + logsource.tags.remove(old_tag) + logsource.tags.add(new_tag) + messages.success(request, f'Tag updated from "{old_tag.name}" to "{new_tag.name}"') + except (AWSLogSource.DoesNotExist, Tag.DoesNotExist): + messages.error(request, 'Error updating tag.') + + if redirect_url: + return redirect(redirect_url) + return redirect('aws:account_resources', account_id=logsource.account.account_id) + +@login_required +def remove_tag_from_resource(request, resource_id, tag_id): + if request.method == 'POST': + redirect_url = request.META.get('HTTP_REFERER', '') + + try: + resource = AWSResource.objects.get(id=resource_id) + tag = Tag.objects.get(id=tag_id) + resource.tags.remove(tag) + messages.success(request, f'Tag "{tag.name}" removed successfully.') + except (AWSResource.DoesNotExist, Tag.DoesNotExist): + messages.error(request, 'Error removing tag.') + + if redirect_url: + return redirect(redirect_url) + return redirect('aws:account_resources', account_id=resource.account.account_id) + +@login_required +def remove_tag_from_credential(request, credential_id, tag_id): + if request.method == 'POST': + redirect_url = request.META.get('HTTP_REFERER', '') + + try: + credential = AWSCredential.objects.get(id=credential_id) + tag = Tag.objects.get(id=tag_id) + credential.tags.remove(tag) + messages.success(request, f'Tag "{tag.name}" removed successfully.') + except (AWSCredential.DoesNotExist, Tag.DoesNotExist): + messages.error(request, 'Error removing tag.') + + if redirect_url: + return redirect(redirect_url) + return redirect('aws:account_resources', account_id=credential.account.account_id) + +@login_required +def remove_tag_from_logsource(request, logsource_id, tag_id): + if request.method == 'POST': + redirect_url = request.META.get('HTTP_REFERER', '') + + try: + logsource = AWSLogSource.objects.get(id=logsource_id) + tag = Tag.objects.get(id=tag_id) + logsource.tags.remove(tag) + messages.success(request, f'Tag "{tag.name}" removed successfully.') + except (AWSLogSource.DoesNotExist, Tag.DoesNotExist): + messages.error(request, 'Error removing tag.') + + if redirect_url: + return redirect(redirect_url) + return redirect('aws:account_resources', account_id=logsource.account.account_id) + diff --git a/apps/data/admin.py b/apps/data/admin.py index 8e47d97..361034d 100644 --- a/apps/data/admin.py +++ b/apps/data/admin.py @@ -1,7 +1,8 @@ from django.contrib import admin -from .models import NormalizedLog, Tag +from .models import NormalizedLog, Tag, DetectionResult # Register your models here. admin.site.register(NormalizedLog) -admin.site.register(Tag) \ No newline at end of file +admin.site.register(Tag) +admin.site.register(DetectionResult) diff --git a/apps/data/migrations/0005_detectionresult.py b/apps/data/migrations/0005_detectionresult.py new file mode 100644 index 0000000..8548e35 --- /dev/null +++ b/apps/data/migrations/0005_detectionresult.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.3 on 2025-02-13 23:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('analysis', '0001_initial'), + ('case', '0001_initial'), + ('data', '0004_alter_normalizedlog_unique_together'), + ] + + operations = [ + migrations.CreateModel( + name='DetectionResult', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('case', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='detection_results', to='case.case')), + ('detection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='analysis.detection')), + ('matched_log', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='detection_matches', to='data.normalizedlog')), + ], + options={ + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['created_at'], name='data_detect_created_0a2c78_idx')], + }, + ), + ] diff --git a/apps/data/migrations/0006_alter_detectionresult_case_and_more.py b/apps/data/migrations/0006_alter_detectionresult_case_and_more.py new file mode 100644 index 0000000..4bc2b9c --- /dev/null +++ b/apps/data/migrations/0006_alter_detectionresult_case_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.3 on 2025-02-14 04:12 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('analysis', '0001_initial'), + ('case', '0001_initial'), + ('data', '0005_detectionresult'), + ] + + operations = [ + migrations.AlterField( + model_name='detectionresult', + name='case', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='case.case'), + ), + migrations.AlterField( + model_name='detectionresult', + name='detection', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='analysis.detection'), + ), + migrations.AlterField( + model_name='detectionresult', + name='matched_log', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.normalizedlog'), + ), + ] diff --git a/apps/data/models.py b/apps/data/models.py index c71a0e0..3ccddbd 100644 --- a/apps/data/models.py +++ b/apps/data/models.py @@ -69,5 +69,21 @@ class Meta: unique_together = (("case", "event_id"),) +class DetectionResult(models.Model): + case = models.ForeignKey('case.Case', on_delete=models.CASCADE) + detection = models.ForeignKey('analysis.Detection', on_delete=models.CASCADE) + matched_log = models.ForeignKey('NormalizedLog', on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['created_at']), + ] + + def __str__(self): + return f"{self.detection.name} - {self.matched_log.event_name}" + + diff --git a/apps/data/urls.py b/apps/data/urls.py index f02d7cc..74d616f 100644 --- a/apps/data/urls.py +++ b/apps/data/urls.py @@ -6,5 +6,8 @@ app_name = 'data' urlpatterns = [ - path("logs", views.NormalizedLogListView, name="normalized_logs"), + path('logs/', views.NormalizedLogListView, name='normalized_logs'), + path('logs//add-tag/', views.add_tag_to_log, name='add_tag_to_log'), + path('logs//edit-tag//', views.edit_log_tag, name='edit_log_tag'), + path('logs//remove-tag//', views.remove_log_tag, name='remove_log_tag'), ] \ No newline at end of file diff --git a/apps/data/views.py b/apps/data/views.py index e95b8d1..74659fb 100644 --- a/apps/data/views.py +++ b/apps/data/views.py @@ -1,40 +1,80 @@ -from django.shortcuts import render +from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db.models import Q -from apps.data.models import NormalizedLog +from apps.data.models import NormalizedLog, Tag +from datetime import datetime +from django.contrib import messages +from apps.aws.models import AWSAccount @login_required def NormalizedLogListView(request): - # Get all logs and apply default sorting + # Get account_id from query params if it exists + account_id = request.GET.get('account_id') + aws_account = None + case = None + queryset = NormalizedLog.objects.all().order_by('-event_time') - # Get filter parameters from request + # Filter by account if specified + if account_id: + aws_account = get_object_or_404(AWSAccount, account_id=account_id) + queryset = queryset.filter(aws_account=aws_account) + case = aws_account.case + search_query = request.GET.get('search', '') field_filter = request.GET.get('field', '') field_value = request.GET.get('field_value', '') sort_order = request.GET.get('sort', '-event_time') + start_date = request.GET.get('start_date', '') + end_date = request.GET.get('end_date', '') + + # Validate sort_order field + valid_sort_fields = [ + 'event_time', '-event_time', + 'event_type', '-event_type', + 'event_source', '-event_source', + 'event_name', '-event_name', + 'user_identity', '-user_identity', + 'region', '-region', + 'ip_address', '-ip_address' + ] + if sort_order not in valid_sort_fields: + sort_order = '-event_time' + + if start_date: + try: + start_date = datetime.strptime(start_date, '%Y-%m-%d') + queryset = queryset.filter(event_time__gte=start_date) + except ValueError: + pass + + if end_date: + try: + end_date = datetime.strptime(end_date, '%Y-%m-%d') + queryset = queryset.filter(event_time__lte=end_date) + except ValueError: + pass - # Apply search if query exists if search_query: queryset = queryset.filter( Q(event_name__icontains=search_query) | Q(event_source__icontains=search_query) | Q(event_type__icontains=search_query) | Q(user_identity__icontains=search_query) | + Q(region__icontains=search_query) | Q(resources__icontains=search_query) ) - # Apply field-specific filter if specified if field_filter and field_value: filter_kwargs = {f"{field_filter}__icontains": field_value} queryset = queryset.filter(**filter_kwargs) - # Apply sorting - queryset = queryset.order_by(sort_order.replace('timestamp', 'event_time')) + queryset = queryset.order_by(sort_order) + + all_tags = Tag.objects.all() - # Pagination - paginator = Paginator(queryset, 50) # Show 50 logs per page + paginator = Paginator(queryset, 100) page_number = request.GET.get('page', 1) page_obj = paginator.get_page(page_number) @@ -45,7 +85,75 @@ def NormalizedLogListView(request): 'field_filter': field_filter, 'field_value': field_value, 'sort_order': sort_order, + 'start_date': start_date, + 'end_date': end_date, 'is_paginated': page_obj.has_other_pages(), + 'all_tags': all_tags, + 'aws_account': aws_account, + 'case': case } return render(request, 'data/normalized_logs.html', context) + +@login_required +def add_tag_to_log(request, log_id): + if request.method == 'POST': + tag_id = request.POST.get('tag_id') + # Get the referer URL with all its query parameters + redirect_url = request.META.get('HTTP_REFERER', '') + + try: + log = NormalizedLog.objects.get(id=log_id) + tag = Tag.objects.get(id=tag_id) + log.tags.add(tag) + messages.success(request, f'Tag "{tag.name}" added successfully.') + except (NormalizedLog.DoesNotExist, Tag.DoesNotExist): + messages.error(request, 'Error adding tag.') + + # If we have a referer URL, redirect back to it to preserve filters + if redirect_url: + return redirect(redirect_url) + + return redirect('data:normalized_logs') + +@login_required +def edit_log_tag(request, log_id, tag_id): + if request.method == 'POST': + new_tag_id = request.POST.get('new_tag_id') + redirect_url = request.META.get('HTTP_REFERER', '') + + try: + log = NormalizedLog.objects.get(id=log_id) + old_tag = Tag.objects.get(id=tag_id) + new_tag = Tag.objects.get(id=new_tag_id) + + # Remove old tag and add new tag + log.tags.remove(old_tag) + log.tags.add(new_tag) + + messages.success(request, f'Tag updated from "{old_tag.name}" to "{new_tag.name}"') + except (NormalizedLog.DoesNotExist, Tag.DoesNotExist): + messages.error(request, 'Error updating tag.') + + if redirect_url: + return redirect(redirect_url) + + return redirect('data:normalized_logs') + +@login_required +def remove_log_tag(request, log_id, tag_id): + if request.method == 'POST': + redirect_url = request.META.get('HTTP_REFERER', '') + + try: + log = NormalizedLog.objects.get(id=log_id) + tag = Tag.objects.get(id=tag_id) + log.tags.remove(tag) + messages.success(request, f'Tag "{tag.name}" removed successfully.') + except (NormalizedLog.DoesNotExist, Tag.DoesNotExist): + messages.error(request, 'Error removing tag.') + + if redirect_url: + return redirect(redirect_url) + + return redirect('data:normalized_logs') diff --git a/scope/urls.py b/scope/urls.py index 9e42f33..6bc3874 100644 --- a/scope/urls.py +++ b/scope/urls.py @@ -39,6 +39,7 @@ path("case/", include("apps.case.urls")), path("aws/", include("apps.aws.urls")), path("data/", include("apps.data.urls")), + path("analysis/", include("apps.analysis.urls")), path("celery-progress/", include("celery_progress.urls")), # API docs path("api/schema/", SpectacularAPIView.as_view(), name="schema"), diff --git a/static/images/cloud/search.svg b/static/images/cloud/search.svg new file mode 100644 index 0000000..1280e60 --- /dev/null +++ b/static/images/cloud/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/logo/icon.png b/static/images/logo/icon.png deleted file mode 100644 index e5aa6dcda5dcdcf469344caf8e309731d7f51653..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69860 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�qc8ld(RhlP?YRh3IK4mv48*)!h!O=#SAT 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

?&%I9JW%pB>c;<48?Ge+1y= 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�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 diff --git a/static/images/logo/icon.svg b/static/images/logo/icon.svg deleted file mode 100644 index 9256439..0000000 --- a/static/images/logo/icon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - 2 - - - - - - \ No newline at end of file diff --git a/static/images/logo/logo-mobile.svg b/static/images/logo/logo-mobile.svg new file mode 100644 index 0000000..10408fc --- /dev/null +++ b/static/images/logo/logo-mobile.svg @@ -0,0 +1,9 @@ + + 1 + + + + + + \ No newline at end of file diff --git a/static/images/logo/logo.png b/static/images/logo/logo.png deleted file mode 100644 index 648a0ec0a4dfbef6c322ce7a42ca0ff1a7855ff8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18521 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 diff --git a/static/images/logo/logo.svg b/static/images/logo/logo.svg index ccb9be3..10408fc 100644 --- a/static/images/logo/logo.svg +++ b/static/images/logo/logo.svg @@ -1,9 +1,9 @@ - + 1 - + - + \ No newline at end of file diff --git a/templates/analysis/case_detections.html b/templates/analysis/case_detections.html new file mode 100644 index 0000000..f9f8410 --- /dev/null +++ b/templates/analysis/case_detections.html @@ -0,0 +1,187 @@ +{% extends "web/app/app_base.html" %} +{% load static %} + +{% block app %} +
+

+ +
+
+

Detections

+

Case {{ case.name }}

+
+
+ + Back to Case + + + Manage Rules + +
+ {% csrf_token %} + +
+
+
+ + +
+
+

Summary

+
+
+
+
+
+
+ +
+
+

Active Detection Rules

+

{{ detection_count }}

+
+
+
+
+
+
+ +
+
+

Total Matches

+

{{ total_results }}

+
+
+
+
+
+
+ + + {% if results_by_detection %} + {% for detection, results in results_by_detection.items %} +
+
+
+

+ {{ detection.name }} + {{ detection.get_severity_display }} +

+

{{ detection.description }}

+
+ {{ results|length }} matches +
+
+
+ + + + + + + + + + + + + + {% for result in results %} + + + + + + + + + + {% endfor %} + +
TimeEvent NameSourceUserIP AddressRegionActions
{{ result.matched_log.event_time|date:"Y-m-d H:i:s" }}{{ result.matched_log.event_name }}{{ result.matched_log.event_source }}{{ result.matched_log.user_identity }}{{ result.matched_log.ip_address }}{{ result.matched_log.region }} +
+ + +
+
+
+
+
+ {% endfor %} + {% else %} +
+
+ +

No Detection Results

+

+ Try running detections or create some detection rules. +

+
+
+ {% endif %} +
+ + + +{% for detection, results in results_by_detection.items %} + {% for result in results %} + + {% endfor %} +{% endfor %} + + +{% for detection, results in results_by_detection.items %} + {% for result in results %} + + {% endfor %} +{% endfor %} +{% endblock %} \ No newline at end of file diff --git a/templates/analysis/detection_confirm_delete.html b/templates/analysis/detection_confirm_delete.html new file mode 100644 index 0000000..e87aafd --- /dev/null +++ b/templates/analysis/detection_confirm_delete.html @@ -0,0 +1,38 @@ +{% extends 'web/app/app_base.html' %} + +{% block app %} +
+ + + +

Delete Detection

+

Case: {{ case.name }}

+ +
+ Are you sure you want to delete the detection rule "{{ detection.name }}"? + This action cannot be undone. +
+ +
+ {% csrf_token %} + + Cancel +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/analysis/detection_form.html b/templates/analysis/detection_form.html new file mode 100644 index 0000000..8a68e06 --- /dev/null +++ b/templates/analysis/detection_form.html @@ -0,0 +1,131 @@ +{% extends 'web/app/app_base.html' %} + +{% block app %} +
+
+ +
+
+

{% if is_create %}Create{% else %}Edit{% endif %} Detection

+

Case {{ case.name }}

+
+ + Back to Rules + +
+ + +
+
+

Detection Details

+
+
+
+ {% csrf_token %} + +
+
+
+ + {{ form.name }} + {% if form.name.errors %} +
{{ form.name.errors }}
+ {% endif %} +
+ +
+ + {{ form.description }} + {% if form.description.errors %} +
{{ form.description.errors }}
+ {% endif %} +
+ +
+ + {{ form.cloud }} + {% if form.cloud.errors %} +
{{ form.cloud.errors }}
+ {% endif %} +
+ +
+ + {{ form.detection_type }} + {% if form.detection_type.errors %} +
{{ form.detection_type.errors }}
+ {% endif %} +
+ +
+ + {{ form.severity }} + {% if form.severity.errors %} +
{{ form.severity.errors }}
+ {% endif %} +
+
+ +
+
+ + {{ form.event_source }} + {% if form.event_source.errors %} +
{{ form.event_source.errors }}
+ {% endif %} +
+ +
+ + {{ form.event_name }} + {% if form.event_name.errors %} +
{{ form.event_name.errors }}
+ {% endif %} +
+ +
+ + {{ form.event_type }} + {% if form.event_type.errors %} +
{{ form.event_type.errors }}
+ {% endif %} +
+ +
+ + {{ form.additional_criteria }} + {% if form.additional_criteria.errors %} +
{{ form.additional_criteria.errors }}
+ {% endif %} +
+ +
+ + {{ form.auto_tags }} + {% if form.auto_tags.errors %} +
{{ form.auto_tags.errors }}
+ {% endif %} +
+ +
+ + {{ form.enabled }} + {% if form.enabled.errors %} +
{{ form.enabled.errors }}
+ {% endif %} +
+
+
+ +
+ Cancel + +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/analysis/detection_list.html b/templates/analysis/detection_list.html new file mode 100644 index 0000000..82296b3 --- /dev/null +++ b/templates/analysis/detection_list.html @@ -0,0 +1,83 @@ +{% extends 'web/app/app_base.html' %} + +{% block app %} +
+
+ +
+
+

Detection Rules

+

Case {{ case.name }}

+
+
+ + Back to Detections + + + Add Detection + +
+ {% csrf_token %} + +
+
+
+ + +
+
+

Detection Rules

+
+
+
+ + + + + + + + + + + + + {% for detection in detections %} + + + + + + + + + {% endfor %} + +
NameCloudTypeSeverityStatusActions
{{ detection.name }}{{ detection.get_cloud_display }}{{ detection.get_detection_type_display }} + + {{ detection.get_severity_display }} + + + {% if detection.enabled %} + Enabled + {% else %} + Disabled + {% endif %} + + +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/aws/account_resources.html b/templates/aws/account_resources.html index 1aeba2d..6ce46e0 100644 --- a/templates/aws/account_resources.html +++ b/templates/aws/account_resources.html @@ -3,160 +3,395 @@ {% load static %} {% block app %} -
- - - +
+
-
-

IAM Users

- +
+
+

IAM Users

+
+
{% if aws_credentials %} -
+
{% for credential in aws_credentials %} -
-
-
-

{{ credential.user }}

-
-
- Password: - {% if credential.password_enabled %} - Enabled - {% else %} - Disabled - {% endif %} -
-
- MFA: - {% if credential.mfa_active %} - Active - {% else %} - Inactive - {% endif %} -
-
- Created: - {{ credential.user_creation_time|date|default:"N/A" }} -
-
- - View Details - +
+
+
+
+

{{ credential.user }}

+
+ {% for tag in credential.tags.all %} + + {{ tag.name }} + + {% empty %} + + {% endfor %} +
+
+
+
+ Password: + + {{ credential.password_enabled|yesno:"Enabled,Disabled" }} + +
+
+ MFA: + + {{ credential.mfa_active|yesno:"Active,Inactive" }} + +
+
+ Created: + {{ credential.user_creation_time|date:"M d, Y"|default:"N/A" }} +
+ + View Details + +
-
+
{% endfor %} -
+
{% else %} -
-

No IAM users found.

-
+
+

No IAM users found in this account.

+
{% endif %} -
+
+ -
-

AWS Resources

- +
+
+

AWS Resources

+
+
{% if grouped_resources %} - {% for resource_type, resources in grouped_resources.items %} -
-

{{ resource_type }}

- -
- {% for resource in resources %} -
-
-
-

- {{ resource.resource_name|default:resource.resource_id }} -

-

Region: {{ resource.aws_region }}

- - View Details - + {% for resource_type, resources in grouped_resources.items %} +

{{ resource_type }}

+
+ {% for resource in resources %} +
+
+
+
+

+ {{ resource.resource_name|default:resource.resource_id }} +

+
+ {% for tag in resource.tags.all %} + + {{ tag.name }} + + {% empty %} + + {% endfor %}
+
+

Region: {{ resource.aws_region }}

+ + View Details +
+
- {% endfor %} + {% endfor %}
-
- {% endfor %} + {% endfor %} {% else %} -
-

No resources found. Would you like to pull resources?

+
+

No resources found. Would you like to pull resources?

- Generate Overview + Generate Overview -
+
{% endif %} -
+ + -
-

AWS Log Sources

- +
+
+

AWS Log Sources

+
+
{% for message in error_messages %} -
{{ message }}
+
{{ message }}
{% endfor %} {% if grouped_log_sources %} - {% for service_name, log_sources in grouped_log_sources.items %} -
-

{{ service_name }}

- -
- {% for log_source in log_sources %} -
-
-
-

{{ log_source.log_name }}

-

Region: {{ log_source.aws_region|default:"Global" }}

-

Status: {{ log_source.status }}

- - View Details - + {% for service_name, log_sources in grouped_log_sources.items %} +

{{ service_name }}

+
+ {% for log_source in log_sources %} +
+
+
+
+

{{ log_source.log_name }}

+
+ {% for tag in log_source.tags.all %} + + {{ tag.name }} + + {% empty %} + + {% endfor %}
+
+
+

Region: {{ log_source.aws_region|default:"Global" }}

+

Status: {{ log_source.status }}

+
+ + View Details +
+
+ {% endfor %} +
+ {% endfor %} + {% else %} +
+

No log sources found.

+
+ {% endif %} +
+
+
+
+ + + +{% for resource_type, resources in grouped_resources.items %} + {% for resource in resources %} + + {% endfor %} +{% endfor %} + + +{% for credential in aws_credentials %} +
+ + + {% endfor %} +{% endfor %} + + +{% for resource_type, resources in grouped_resources.items %} + {% for resource in resources %} + {% for tag in resource.tags.all %} + + {% endfor %} + {% endfor %} +{% endfor %} + + +{% for credential in aws_credentials %} + {% for tag in credential.tags.all %} + + {% endfor %} +{% endfor %} + + +{% for service_name, log_sources in grouped_log_sources.items %} + {% for log_source in log_sources %} + {% for tag in log_source.tags.all %} + + {% endfor %} + {% endfor %} +{% endfor %} - - -
{% endblock %} diff --git a/templates/aws/connect_aws.html b/templates/aws/connect_aws.html index c74a4cc..ee382fa 100644 --- a/templates/aws/connect_aws.html +++ b/templates/aws/connect_aws.html @@ -3,15 +3,52 @@ {% load static %} {% block app %} -
-

Connect AWS to Case: {{ case.name }}

-
- {% csrf_token %} - {{ form.as_p }} -
- - Cancel -
-
+
+
+ +
+
+

Connect AWS Account

+

{{ case.name }}

+
+ + Back to Case + +
+ + +
+
+
+ {% csrf_token %} + + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% if field.errors %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} + {% endif %} +
+ {% endfor %} + +
+ Cancel + +
+
+
+
+
+
{% endblock %} diff --git a/templates/aws/credential_details.html b/templates/aws/credential_details.html index 04481b1..71fdc4d 100644 --- a/templates/aws/credential_details.html +++ b/templates/aws/credential_details.html @@ -3,160 +3,138 @@ {% load static %} {% block app %} -
- - - +
+
-

IAM User Details: {{ credential.user }}

+
+

IAM User Details

+

{{ credential.user }}

+
+ + Back to Account +
- -
-
-
-

Basic Information

-
-
-
-
Username
-
{{ credential.user }}
- -
ARN
-
{{ credential.user_arn }}
- -
Created
-
{{ credential.user_creation_time|default:"N/A" }}
-
-
-
+ +
+
+
+

Basic Information

+
+
+
+
Username
+
{{ credential.user }}
+ +
ARN
+
{{ credential.user_arn }}
+ +
Created
+
{{ credential.user_creation_time|default:"N/A" }}
+
+
- - -
-
-
-

Password Status

-
-
-
-
Password Enabled
-
- {% if credential.password_enabled %} - Yes - {% else %} - No - {% endif %} -
- -
Last Used
-
{{ credential.password_last_used|default:"Never" }}
- -
Last Changed
-
{{ credential.password_last_changed|default:"Never" }}
- -
Next Rotation
-
{{ credential.password_next_rotation_date|default:"Not Set" }}
- -
MFA Active
-
- {% if credential.mfa_active %} - Yes - {% else %} - No - {% endif %} -
-
-
-
+
+ + +
+
+
+

Password Status

+
+
+
+
Password Enabled
+
+ + {{ credential.password_enabled|yesno:"Yes,No" }} + +
+ +
Last Used
+
{{ credential.password_last_used|default:"Never" }}
+ +
Last Changed
+
{{ credential.password_last_changed|default:"Never" }}
+ +
Next Rotation
+
{{ credential.password_next_rotation_date|default:"Not Set" }}
+ +
MFA Active
+
+ + {{ credential.mfa_active|yesno:"Yes,No" }} + +
+
+
- - -
-
-
-

Access Key 1

-
-
-
-
Status
-
- {% if credential.access_key_1_active %} - Active - {% else %} - Inactive - {% endif %} -
- -
Last Rotated
-
{{ credential.access_key_1_last_rotated|default:"Never" }}
- -
Last Used
-
{{ credential.access_key_1_last_used_date|default:"Never" }}
- -
Last Used Region
-
{{ credential.access_key_1_last_used_region|default:"N/A" }}
- -
Last Used Service
-
{{ credential.access_key_1_last_used_service|default:"N/A" }}
-
-
-
+
+ + +
+
+
+

Access Key 1

+
+
+
+
Status
+
+ + {{ credential.access_key_1_active|yesno:"Active,Inactive" }} + +
+ +
Last Rotated
+
{{ credential.access_key_1_last_rotated|default:"Never" }}
+ +
Last Used
+
{{ credential.access_key_1_last_used_date|default:"Never" }}
+ +
Last Used Region
+
{{ credential.access_key_1_last_used_region|default:"N/A" }}
+ +
Last Used Service
+
{{ credential.access_key_1_last_used_service|default:"N/A" }}
+
+
- - -
-
-
-

Access Key 2

-
-
-
-
Status
-
- {% if credential.access_key_2_active %} - Active - {% else %} - Inactive - {% endif %} -
- -
Last Rotated
-
{{ credential.access_key_2_last_rotated|default:"Never" }}
- -
Last Used
-
{{ credential.access_key_2_last_used_date|default:"Never" }}
- -
Last Used Region
-
{{ credential.access_key_2_last_used_region|default:"N/A" }}
- -
Last Used Service
-
{{ credential.access_key_2_last_used_service|default:"N/A" }}
-
-
-
+
+ + +
+
+
+

Access Key 2

+
+
+
+
Status
+
+ + {{ credential.access_key_2_active|yesno:"Active,Inactive" }} + +
+ +
Last Rotated
+
{{ credential.access_key_2_last_rotated|default:"Never" }}
+ +
Last Used
+
{{ credential.access_key_2_last_used_date|default:"Never" }}
+ +
Last Used Region
+
{{ credential.access_key_2_last_used_region|default:"N/A" }}
+ +
Last Used Service
+
{{ credential.access_key_2_last_used_service|default:"N/A" }}
+
+
+
+
{% endblock %} \ No newline at end of file diff --git a/templates/aws/edit_account.html b/templates/aws/edit_account.html index cfe6b17..6fa5e13 100644 --- a/templates/aws/edit_account.html +++ b/templates/aws/edit_account.html @@ -3,16 +3,52 @@ {% load static %} {% block app %} -
-

Edit AWS Account: {{ account.account_id }}

-
- {% csrf_token %} - {{ form.as_p }} -
- - Cancel -
-
-
+
+
+ +
+
+

Edit AWS Account

+

{{ account.account_id }}

+
+ + Back to Case + +
+ + +
+
+
+ {% csrf_token %} + + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% if field.errors %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} + {% endif %} +
+ {% endfor %} + +
+ Cancel + +
+
+
+
+
+
{% endblock %} diff --git a/templates/aws/fetch_cloudtrail_logs.html b/templates/aws/fetch_cloudtrail_logs.html index 36dd035..71ef061 100644 --- a/templates/aws/fetch_cloudtrail_logs.html +++ b/templates/aws/fetch_cloudtrail_logs.html @@ -2,107 +2,262 @@ {% load i18n %} {% load static %} {% block app %} -
-
-
-

Select S3 Bucket & Date Range (Account: {{ account_id }})

-
- {% csrf_token %} -
- {{ form.resource.label_tag }} - {{ form.resource }} -
- -
- {{ form.prefix.label_tag }} - {{ form.prefix }} - {{ form.prefix.help_text }} -
- -
-
- {{ form.start_date.label_tag }} - {{ form.start_date }} +
+
+ +
+
+

Fetch CloudTrail Logs

+

Account {{ aws_account.account_id }}

+
+ + Back to Logs + +
+ + +
+
+

CloudTrail Configuration

+
+
+ + {% csrf_token %} + +
+ + {{ form.resource }} + {% if form.resource.help_text %} +
{{ form.resource.help_text }}
+ {% endif %}
-
- {{ form.end_date.label_tag }} - {{ form.end_date }} + +
+
+ + {{ form.start_date }} +
+
+ + {{ form.end_date }} +
-
- - -
+
+ +
+ + + + + +
+
-
-

Browse Bucket Structure

-
-

Select a bucket on the left, then click "Load Root" to see subfolders.

- -
+
+
+ +
+

Select bucket and dates to see suggested prefixes...

+
+
+
+ + + +
+ Cancel + +
+
-
+
- {% endblock %} diff --git a/templates/aws/get_logs.html b/templates/aws/get_logs.html index 6f28311..182cb30 100644 --- a/templates/aws/get_logs.html +++ b/templates/aws/get_logs.html @@ -3,122 +3,127 @@ {% load static %} {% block app %} -
- - - +
+
-

Logs for Account: {{ aws_account.account_id }}

- +
+

AWS Logs

+

Account {{ aws_account.account_id }}

+
+
-
-
-
- - +
+
+

Date Range

+
+
+ +
+
+ +
-
- - +
+ +
-
- +
+
-
- +
+ +
+
- +
- -
-
-
-

Top 10 Users

-
    - {% for user in top_users %} -
  • - {{ user.user_identity|default:"Unknown User" }} - {{ user.count }} -
  • - {% empty %} -
  • - No users found. -
  • - {% endfor %} -
+ +
+
+
+

Top Users

+
+
+
+ {% for user in top_users %} +
+ {{ user.user_identity|default:"Unknown User" }} + {{ user.count }} +
+ {% empty %} +
+ No users found
+ {% endfor %}
+
+
- -
-
-
-

Top 10 IPs

-
    - {% for ip in top_ips %} -
  • - {{ ip.ip_address|default:"Unknown IP" }} - {{ ip.count }} -
  • - {% empty %} -
  • - No IPs found. -
  • - {% endfor %} -
+ +
+
+
+

Top IP Addresses

+
+
+
+ {% for ip in top_ips %} +
+ {{ ip.ip_address|default:"Unknown IP" }} + {{ ip.count }}
+ {% empty %} +
+ No IP addresses found +
+ {% endfor %}
+
+
- -
-
-
-

Top 10 Events

-
    - {% for event in top_events %} -
  • - {{ event.event_name|default:"Unknown Event" }} - {{ event.count }} -
  • - {% empty %} -
  • - No events found. -
  • - {% endfor %} -
+ +
+
+
+

Top Events

+
+
+
+ {% for event in top_events %} +
+ {{ event.event_name|default:"Unknown Event" }} + {{ event.count }} +
+ {% empty %} +
+ No events found
+ {% endfor %}
+
+
- - +
{% endblock %} diff --git a/templates/aws/logsource_details.html b/templates/aws/logsource_details.html index 8619304..82154ea 100644 --- a/templates/aws/logsource_details.html +++ b/templates/aws/logsource_details.html @@ -2,44 +2,64 @@ {% load i18n %} {% load static %} {% block app %} -
-

Log Source Details

+
+
+ +
+
+

Log Source Details

+

{{ log_source.log_name }}

+
+ + Back to Account + +
+ + +
+
+

Basic Information

+
+
+
+
Service Name
+
{{ log_source.service_name }}
+ +
Status
+
{{ log_source.status }}
- -
-

{{ log_source.log_name }}

-

Service Name: {{ log_source.service_name }}

-

Status: {{ log_source.status }}

-

Region: {{ log_source.aws_region|default:"Global" }}

+
Region
+
{{ log_source.aws_region|default:"Global" }}
+
+
- -
-

Log Source Details

-
    - {% for key, value in log_source.log_details.items %} -
  • - {{ key }}: - - {% if value is iterable and value.items %} - -
      - {% for nested_key, nested_value in value.items %} -
    • - {{ nested_key }}: - {{ nested_value }} -
    • - {% endfor %} -
    - {% elif value is iterable and value|length > 50 %} -
    {{ value }}
    - {% else %} - {{ value }} - {% endif %} -
    -
  • - {% endfor %} -
+ +
+
+

Log Source Details

+
+
+ {% for key, value in log_source.log_details.items %} +
+

{{ key }}

+ {% if value is iterable and value.items %} + +
+ {% for nested_key, nested_value in value.items %} +
{{ nested_key }}
+
{{ nested_value }}
+ {% endfor %} +
+ {% elif value is iterable and value|length > 50 %} +
{{ value }}
+ {% else %} +

{{ value }}

+ {% endif %} +
+ {% endfor %} +
-
+
+
{% endblock %} diff --git a/templates/aws/resource_details.html b/templates/aws/resource_details.html index 5f3be75..e958bac 100644 --- a/templates/aws/resource_details.html +++ b/templates/aws/resource_details.html @@ -2,43 +2,61 @@ {% load i18n %} {% load static %} {% block app %} -
-

Resource Details

+
+
+ +
+
+

Resource Details

+

{{ resource.resource_name|default:resource.resource_id }}

+
+ + Back to Account + +
+ + +
+
+

Basic Information

+
+
+
+
Resource Type
+
{{ resource.resource_type }}
- -
-

{{ resource.resource_name|default:resource.resource_id }}

-

Resource Type: {{ resource.resource_type }}

-

Region: {{ resource.aws_region }}

+
Region
+
{{ resource.aws_region }}
+
+
- -
-

Resource Details

-
    - {% for key, value in resource.resource_details.items %} -
  • - {{ key }}: - - {% if value is iterable and value.items %} - -
      - {% for nested_key, nested_value in value.items %} -
    • - {{ nested_key }}: - {{ nested_value }} -
    • - {% endfor %} -
    - {% elif value is iterable and value|length > 50 %} -
    {{ value }}
    - {% else %} - {{ value }} - {% endif %} -
    -
  • - {% endfor %} -
+ +
+
+

Resource Details

+
+
+ {% for key, value in resource.resource_details.items %} +
+

{{ key }}

+ {% if value is iterable and value.items %} + +
+ {% for nested_key, nested_value in value.items %} +
{{ nested_key }}
+
{{ nested_value }}
+ {% endfor %} +
+ {% elif value is iterable and value|length > 50 %} +
{{ value }}
+ {% else %} +

{{ value }}

+ {% endif %} +
+ {% endfor %} +
-
+
+
{% endblock %} diff --git a/templates/case/case_detail.html b/templates/case/case_detail.html index c5ecafa..3bdb41e 100644 --- a/templates/case/case_detail.html +++ b/templates/case/case_detail.html @@ -3,92 +3,141 @@ {% load static %} {% block app %} -
- - - - -
- -
-

{{ case.name }}

-

Status: {{ case.status }}

-

Created At: {{ case.created_at|date:"Y-m-d H:i" }}

-

Case ID: {{ case.uuid }}

-

{{ case.description }}

+
+
+ +
+
+

{{ case.name }}

+

Case Details

+
+
- -
- Edit Case - Connect Client + +
+
+
+
+
+
Status
+
{{ case.status }}
+
Created
+
{{ case.created_at|date:"M d, Y H:i" }}
+
Case ID
+
{{ case.uuid }}
+ {% if case.description %} +
Description
+
{{ case.description }}
+ {% endif %} +
+
+
+
-
- -

Connected Accounts

- {% if aws_accounts %} -
- {% for account in aws_accounts %} -
-
- -
-
- AWS Account: {{ account.account_id }} -
-

- Region: {{ account.aws_region }} | - Added By: {{ account.added_by.username }} | - Added At: {{ account.added_at|date:"Y-m-d H:i" }} -

- {% if account.validated %} - Validated - {% else %} - Not Validated - {% endif %} + +
+
+

Connected Accounts

+
+ + {% if aws_accounts %} +
+ {% for account in aws_accounts %} +
+
+
+
+ AWS +

AWS Account: {{ account.account_id }}

+ {% if account.validated %} + Validated + {% else %} + Not Validated + {% endif %} +
+

+ Region: {{ account.aws_region }} | + Added by {{ account.added_by.username }} on {{ account.added_at|date:"M d, Y" }} +

+
+ +
+ + + + + +
+
+
+ {% endfor %}
- - -
- Edit - - - Overview - - - Logging - + {% else %} +
+

No accounts connected yet. Use "Connect Client" to add accounts.

-
+ {% endif %}
- {% endfor %}
- {% else %} -

No accounts connected yet. Use "Connect Client" to add accounts.

- {% endif %} +
- -

GCP Accounts

- {% if gcp_placeholder %} -

GCP integration coming soon.

- {% endif %} +{% block extra_js %} + +{% endblock %} {% endblock %} diff --git a/templates/case/connect_client.html b/templates/case/connect_client.html index d000810..cfd0fe2 100644 --- a/templates/case/connect_client.html +++ b/templates/case/connect_client.html @@ -3,21 +3,36 @@ {% load static %} {% block app %} -
-

Connect a Client to Case: {{ case.name }}

+
+
+ +
+
+

Connect Client

+

{{ case.name }}

+
+ + Back to Case + +
+ +
- -
-
-
- AWS -

AWS

- Connect AWS -
-
+ +
+
+
+ AWS +

Amazon Web Services

+ + Connect AWS + +
- +
+
-
+
+
{% endblock %} diff --git a/templates/case/create_case.html b/templates/case/create_case.html index 44fd3c4..df54d74 100644 --- a/templates/case/create_case.html +++ b/templates/case/create_case.html @@ -3,29 +3,58 @@ {% load static %} {% block app %} -
-

Create a New Case

-
- {% csrf_token %} -
- - -
-
- - -
-
- +
+
+ +
+
+

Create New Case

+

Start a new investigation case

+
+
+ + +
+
+ + {% csrf_token %} + +
+ + +
+ +
+ + +
+ +
+ -
-
- -
- -
+
+ +
+ +
+ +
+
+
+
{% endblock %} diff --git a/templates/case/edit_case.html b/templates/case/edit_case.html index 4a3ce2c..64b5b30 100644 --- a/templates/case/edit_case.html +++ b/templates/case/edit_case.html @@ -3,16 +3,54 @@ {% load static %} {% block app %} -
-

Edit Case: {{ case.name }}

-
- {% csrf_token %} - {{ form.as_p }} -
- - Cancel -
-
-
+
+
+ +
+
+

Edit Case

+

{{ case.name }}

+
+ + Back to Case + +
+ + +
+
+
+ {% csrf_token %} + + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% if field.errors %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} + {% endif %} +
+ {% endfor %} + +
+ + Cancel + + +
+
+
+
+
+
{% endblock %} diff --git a/templates/data/normalized_logs.html b/templates/data/normalized_logs.html index 8c16140..a011b7c 100644 --- a/templates/data/normalized_logs.html +++ b/templates/data/normalized_logs.html @@ -1,98 +1,253 @@ {% extends "web/app/app_base.html" %} {% load static %} -{% block content %} -
-

Normalized Logs

- - -
-
+{% block app %} +
+
+ +
+
+

Log Explorer

+
+ + Back to Account + +
+ + +
+
+

Search & Filters

+
+
+ +
-
- -
+ +
- + +
- + +
- + +
-
- - +
+ +
+ From + + To + +
+
+
+ +
+
+ +
+
- -
- + +
+
+

Log Entries

+
+
+
+
- - - - - - - + + + + + + + + + + + - {% for log in object_list %} + {% for log in object_list %} - - - - - + + + + + + + + + - {% empty %} + {% empty %} - + - {% endfor %} + {% endfor %} -
TimestampLevelSourceResource IDMessage
ActionsEvent TimeEvent TypeEvent SourceEvent NameUser IdentityRegionIP AddressResources
{{ log.timestamp }} - {{ log.log_level }} - {{ log.source }}{{ log.resource_id }}{{ log.message }} + + {% for tag in log.tags.all %} + + {{ tag.name }} + + {% empty %} + + {% endfor %} + {{ log.event_time }}{{ log.event_type }}{{ log.event_source }}{{ log.event_name }}{{ log.user_identity }}{{ log.region }}{{ log.ip_address }}{{ log.resources|truncatechars:50 }}
No logs found.No logs found matching your criteria.
+ +
+
{% if is_paginated %} - {% endif %} +
+
+ + +{% for log in object_list %} + + + + +{% endfor %} + + +{% for log in object_list %} + {% for tag in log.tags.all %} + + {% endfor %} +{% endfor %} + {% endblock %} \ No newline at end of file diff --git a/templates/web/app/app_base.html b/templates/web/app/app_base.html index b795c27..ca16bf9 100644 --- a/templates/web/app/app_base.html +++ b/templates/web/app/app_base.html @@ -5,7 +5,7 @@ {% endblock %}
-
+
{% block app %} {% endblock %}
diff --git a/templates/web/app_home.html b/templates/web/app_home.html index 82314c0..ea818cf 100644 --- a/templates/web/app_home.html +++ b/templates/web/app_home.html @@ -3,44 +3,71 @@ {% load static %} {% block app %} -
-
- +
+
+
-

Your Cases

- Create New Case +
+

Investigation Cases

+

Manage and track your investigation cases

+
+ + New Case +
- + {% if cases %} -
- - - - - - - - - - - {% for case in cases %} - - - - - - - {% endfor %} - -
NameStatusCreated AtActions
{{ case.name }}{{ case.status }}{{ case.created_at|date:"Y-m-d H:i" }} - View Case -
-
+
+
+ + + + + + + + + + + {% for case in cases %} + + + + + + + {% endfor %} + +
Case NameStatusCreatedActions
+
+ + {{ case.name }} +
+
+ + {{ case.status }} + + {{ case.created_at|date:"M d, Y" }} + + Open + +
+
+
{% else %} - +
+
+ No cases +

No Cases Yet

+

Start by creating your first investigation case

+ + Create New Case + +
+
{% endif %}
diff --git a/templates/web/components/hero.html b/templates/web/components/hero.html index 7ff5ddf..cdc0cff 100644 --- a/templates/web/components/hero.html +++ b/templates/web/components/hero.html @@ -1,23 +1,24 @@ {% load i18n %} {% load static %} -
+ +
+
diff --git a/templates/web/components/top_nav.html b/templates/web/components/top_nav.html index 23a70b1..74f8042 100644 --- a/templates/web/components/top_nav.html +++ b/templates/web/components/top_nav.html @@ -2,7 +2,7 @@