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 9c93512..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) @@ -179,19 +190,25 @@ 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) +# 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,10 +316,26 @@ 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 @@ -248,7 +345,11 @@ def fetch_cloudtrail_logs(request, 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): @@ -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 new file mode 100644 index 0000000..74d616f --- /dev/null +++ b/apps/data/urls.py @@ -0,0 +1,13 @@ +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'), + 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 91ea44a..74659fb 100644 --- a/apps/data/views.py +++ b/apps/data/views.py @@ -1,3 +1,159 @@ -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, Tag +from datetime import datetime +from django.contrib import messages +from apps.aws.models import AWSAccount -# Create your views here. +@login_required +def NormalizedLogListView(request): + # 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') + + # 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 + + 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) + ) + + if field_filter and field_value: + filter_kwargs = {f"{field_filter}__icontains": field_value} + queryset = queryset.filter(**filter_kwargs) + + queryset = queryset.order_by(sort_order) + + all_tags = Tag.objects.all() + + paginator = Paginator(queryset, 100) + 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, + '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 f7a9a64..6bc3874 100644 --- a/scope/urls.py +++ b/scope/urls.py @@ -38,6 +38,8 @@ path("", include("apps.web.urls")), 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 e5aa6dc..0000000 Binary files a/static/images/logo/icon.png and /dev/null differ 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 648a0ec..0000000 Binary files a/static/images/logo/logo.png and /dev/null differ 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 17645d4..6ce46e0 100644 --- a/templates/aws/account_resources.html +++ b/templates/aws/account_resources.html @@ -3,200 +3,395 @@ {% load static %} {% block app %} -
- - - +
+
- -
-

AWS Resources

+ +
+
+

IAM Users

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

{{ 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 in this account.

+
+ {% endif %} +
+
+ +
+
+

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 %} - - - - - - - - - - {% 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
+
+
+ {% 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 new file mode 100644 index 0000000..182cb30 --- /dev/null +++ b/templates/aws/get_logs.html @@ -0,0 +1,129 @@ +{% extends "web/app/app_base.html" %} +{% load i18n %} +{% load static %} + +{% block app %} +
+
+ +
+
+

AWS Logs

+

Account {{ aws_account.account_id }}

+
+ +
+ + +
+
+

Date Range

+
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ + +
+ +
+
+
+

Top Users

+
+
+
+ {% for user in top_users %} +
+ {{ user.user_identity|default:"Unknown User" }} + {{ user.count }} +
+ {% empty %} +
+ No users 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 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/normalized_logs.html b/templates/aws/normalized_logs.html deleted file mode 100644 index b175ecd..0000000 --- a/templates/aws/normalized_logs.html +++ /dev/null @@ -1,118 +0,0 @@ -{% extends "web/app/app_base.html" %} -{% load i18n %} -{% load static %} - -{% block app %} -
- - - - -
-

Logs for Account: {{ aws_account.account_id }}

- -
- - -
-
-
- - -
-
- - -
-
- -
-
-
- - -
- -
-
-
-

Top 10 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 10 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/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 new file mode 100644 index 0000000..a011b7c --- /dev/null +++ b/templates/data/normalized_logs.html @@ -0,0 +1,253 @@ +{% extends "web/app/app_base.html" %} +{% load static %} + +{% block app %} +
+
+ +
+
+

Log Explorer

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

Search & Filters

+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ From + + To + +
+
+
+ +
+
+
+
+
+ + +
+
+

Log Entries

+
+
+
+ + + + + + + + + + + + + + + + {% for log in object_list %} + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
ActionsEvent TimeEvent TypeEvent SourceEvent NameUser IdentityRegionIP AddressResources
+ + {% 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 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 @@