Compare commits

..

9 commits

Author SHA1 Message Date
pedro 9247f11c27 Merge pull request 'docker-add-idhub' (#43) from docker-add-idhub into main
Reviewed-on: #43
2025-01-29 17:57:48 +00:00
pedro 56d8aadf83 docker: docker for devicehub and idhub as profile 2025-01-27 17:52:24 +01:00
pedro 601da538bf add idhub to dockercompose and its .env.example 2025-01-23 12:56:55 +01:00
pedro 754820f631 Merge pull request 'upload_legacy_snapshot' (#40) from upload_legacy_snapshot into main
Reviewed-on: #40
2025-01-23 11:25:18 +00:00
pedro 01db6a3e8e add all snapshots that we support
coauthored with cayo
2025-01-23 12:24:18 +01:00
Cayo Puigdefabregas 9817d4eb45 fix credential snapshot 2025-01-23 12:24:18 +01:00
Cayo Puigdefabregas 0d60afaa35 split parse files for cases of use 2025-01-23 12:24:18 +01:00
Cayo Puigdefabregas 918cf73506 refactor legacy 2025-01-23 12:24:18 +01:00
pedro 9d963e7f54 docker: facilitate multiple instances in same host
start using namespace because more things are going to come in same
docker compose
2025-01-21 14:21:22 +01:00
70 changed files with 1925 additions and 2608 deletions

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
db
.git

View file

@ -1,8 +1,18 @@
DOMAIN=localhost
####
# DEV OPTIONS
####
DEV_DOCKER_ALWAYS_BUILD=false
####
# DEVICEHUB
####
DEVICEHUB_DOMAIN=localhost
DEVICEHUB_PORT=8001
DEMO=true
# note that with DEBUG=true, logs are more verbose (include tracebacks)
DEBUG=true
ALLOWED_HOSTS=${DOMAIN},${DOMAIN}:8000,127.0.0.1,127.0.0.1:8000
DPP=false
STATIC_ROOT=/tmp/static/
@ -16,6 +26,46 @@ EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
EMAIL_FILE_PATH="/tmp/app-messages"
ENABLE_EMAIL=false
PREDEFINED_TOKEN='5018dd65-9abd-4a62-8896-80f34ac66150'
DEVICEHUB_ALLOWED_HOSTS=${DEVICEHUB_DOMAIN},${DEVICEHUB_DOMAIN}:${DEVICEHUB_PORT},127.0.0.1,127.0.0.1:${DEVICEHUB_PORT}
# TODO review these vars
#SNAPSHOTS_DIR=/path/to/TODO
#EVIDENCES_DIR=/path/to/TODO
####
# IDHUB
####
IDHUB_ENABLED=false
IDHUB_DOMAIN=localhost
IDHUB_PORT=9001
IDHUB_ALLOWED_HOSTS=${IDHUB_DOMAIN},${IDHUB_DOMAIN}:${IDHUB_PORT},127.0.0.1,127.0.0.1:${IDHUB_PORT}
IDHUB_TIME_ZONE='Europe/Madrid'
#IDHUB_SECRET_KEY='uncomment-it-and-fill-this'
# enable dev flags when DEVELOPMENT deployment
# adapt to your domain in a production/reverse proxy env
IDHUB_CSRF_TRUSTED_ORIGINS='https://idhub.example.org'
# fill this section with your email credentials
IDHUB_DEFAULT_FROM_EMAIL="user@example.org"
IDHUB_EMAIL_HOST="smtp.example.org"
IDHUB_EMAIL_HOST_USER="smtp_user"
IDHUB_EMAIL_HOST_PASSWORD="smtp_passwd"
IDHUB_EMAIL_PORT=25
IDHUB_EMAIL_USE_TLS=True
IDHUB_EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
# replace with production data
# this is used when IDHUB_DEPLOYMENT is not equal to DEVELOPMENT
IDHUB_ADMIN_USER='admin'
IDHUB_ADMIN_PASSWD='admin'
IDHUB_ADMIN_EMAIL='admin@example.org'
# this option needs to be set to 'n' to be able to make work idhub in docker
# by default it is set to 'y' to facilitate idhub dev when outside docker
IDHUB_SYNC_ORG_DEV='n'
# TODO that is only for testing
IDHUB_ENABLE_EMAIL=false
IDHUB_ENABLE_2FACTOR_AUTH=false
IDHUB_ENABLE_DOMAIN_CHECKER=false

View file

@ -1,16 +0,0 @@
from django import forms
from .models import State
class ChangeStateForm(forms.Form):
previous_state = forms.CharField(widget=forms.HiddenInput())
snapshot_uuid = forms.UUIDField(widget=forms.HiddenInput())
new_state = forms.CharField(widget=forms.HiddenInput())
class AddNoteForm(forms.Form):
snapshot_uuid = forms.UUIDField(widget=forms.HiddenInput())
note = forms.CharField(
required=True,
widget=forms.Textarea(attrs={'rows': 4, 'maxlength': 200, 'placeholder': 'Max 200 characters'}),
)

View file

@ -1,43 +0,0 @@
#!/usr/bin/env python3
import logging
from django.core.management.base import BaseCommand
from action.models import StateDefinition, Institution
from django.utils.translation import gettext as _
logger = logging.getLogger('django')
class Command(BaseCommand):
help = 'Create default StateDefinitions for a given institution. "'
def add_arguments(self, parser):
parser.add_argument('institution_name', type=str, help='The name of the institution')
def handle(self, *args, **kwargs):
default_states = [
_("INBOX"),
_("VISUAL INSPECTION"),
_("REPAIR"),
_("INSTALL"),
_("TEST"),
_("PACKAGING"),
_("DONATION"),
_("DISMANTLE")
]
institution_name = kwargs['institution_name']
institution = Institution.objects.filter(name=institution_name).first()
if not institution:
txt = "No institution found for: %s. Please create an institution first"
logger.error(txt, institution.name)
return
for state in default_states:
state_def, created = StateDefinition.objects.get_or_create(
institution=institution,
state=state
)
if created:
self.stdout.write(self.style.SUCCESS(f'Successfully created state: {state}'))
else:
self.stdout.write(self.style.WARNING(f'State already exists: {state}'))

View file

@ -1,83 +0,0 @@
# Generated by Django 5.0.6 on 2024-12-11 18:05
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("user", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="State",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date", models.DateTimeField(auto_now_add=True)),
("state", models.CharField(max_length=50)),
("snapshot_uuid", models.UUIDField()),
(
"institution",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="user.institution",
),
),
(
"user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="StateDefinition",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("order", models.PositiveIntegerField(default=0)),
("state", models.CharField(max_length=50)),
(
"institution",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="user.institution",
),
),
],
options={
"ordering": ["order"],
},
),
migrations.AddConstraint(
model_name="statedefinition",
constraint=models.UniqueConstraint(
fields=("institution", "state"), name="unique_institution_state"
),
),
]

View file

@ -1,89 +0,0 @@
# Generated by Django 5.0.6 on 2024-12-17 19:40
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("action", "0001_initial"),
("user", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="DeviceLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date", models.DateTimeField(auto_now_add=True)),
("event", models.CharField(max_length=255)),
("snapshot_uuid", models.UUIDField()),
(
"institution",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="user.institution",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-date"],
},
),
migrations.CreateModel(
name="Note",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date", models.DateTimeField(auto_now_add=True)),
("description", models.TextField()),
("snapshot_uuid", models.UUIDField()),
(
"institution",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="user.institution",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-date"],
},
),
]

View file

@ -1,82 +1,3 @@
from django.db import models, connection
from django.db.models import Max
from user.models import User, Institution
from django.core.exceptions import ValidationError
from django.db import models
class State(models.Model):
date = models.DateTimeField(auto_now_add=True)
institution = models.ForeignKey(Institution, on_delete=models.SET_NULL, null=True)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
state = models.CharField(max_length=50)
snapshot_uuid = models.UUIDField()
def clean(self):
if not StateDefinition.objects.filter(institution=self.institution, state=self.state).exists():
raise ValidationError(f"The state '{self.state}' is not valid for the institution '{self.institution.name}'.")
def save(self, *args, **kwargs):
self.clean()
super().save(*args, **kwargs)
def __str__(self):
return f"{self.institution.name} - {self.state} - {self.snapshot_uuid}"
class StateDefinition(models.Model):
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
order = models.PositiveIntegerField(default=0)
state = models.CharField(max_length=50)
class Meta:
ordering = ['order']
constraints = [
models.UniqueConstraint(fields=['institution', 'state'], name='unique_institution_state')
]
def save(self, *args, **kwargs):
if not self.pk:
# set the order to be last
max_order = StateDefinition.objects.filter(institution=self.institution).aggregate(Max('order'))['order__max']
self.order = (max_order or 0) + 1
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
institution = self.institution
order = self.order
super().delete(*args, **kwargs)
# Adjust the order of other instances
StateDefinition.objects.filter(institution=institution, order__gt=order).update(order=models.F('order') - 1)
def __str__(self):
return f"{self.institution.name} - {self.state}"
class Note(models.Model):
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
date = models.DateTimeField(auto_now_add=True)
description = models.TextField()
snapshot_uuid = models.UUIDField()
class Meta:
ordering = ['-date']
def __str__(self):
return f" Note: {self.description}, by {self.user.username} @ {self.user.institution} - {self.date}, for {self.snapshot_uuid}"
class DeviceLog(models.Model):
institution = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
date = models.DateTimeField(auto_now_add=True)
event = models.CharField(max_length=255)
snapshot_uuid = models.UUIDField()
class Meta:
ordering = ['-date']
def __str__(self):
return f"{self.event} by {self.user.username} @ {self.institution.name} - {self.date}, for {self.snapshot_uuid}"
# Create your models here.

View file

@ -1,12 +1 @@
from django.urls import path, include
from action import views
app_name = 'action'
urlpatterns = [
path("new/", views.ChangeStateView.as_view(), name="change_state"),
path('note/add/', views.AddNoteView.as_view(), name='add_note'),
path('note/edit/<int:pk>', views.UpdateNoteView.as_view(), name='update_note'),
path('note/delete/<int:pk>', views.DeleteNoteView.as_view(), name='delete_note'),
]

View file

@ -1,140 +1 @@
from django.views import View
from django.shortcuts import redirect, get_object_or_404
from django.contrib import messages
from action.forms import ChangeStateForm, AddNoteForm
from django.views.generic.edit import DeleteView, CreateView, UpdateView, FormView
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from action.models import State, StateDefinition, Note, DeviceLog
from device.models import Device
class ChangeStateView(FormView):
form_class = ChangeStateForm
def form_valid(self, form):
previous_state = form.cleaned_data['previous_state']
new_state = form.cleaned_data['new_state']
snapshot_uuid = form.cleaned_data['snapshot_uuid']
State.objects.create(
snapshot_uuid=snapshot_uuid,
state=new_state,
user=self.request.user,
institution=self.request.user.institution,
)
message = _("<Created> State '{}'. Previous State: '{}'").format(new_state, previous_state)
DeviceLog.objects.create(
snapshot_uuid=snapshot_uuid,
event=message,
user=self.request.user,
institution=self.request.user.institution,
)
messages.success(self.request, _("State successfully changed from '{}' to '{}'").format(previous_state, new_state))
return super().form_valid(form)
def form_invalid(self, form):
messages.error(self.request, _("There was an error with your submission."))
return redirect(self.get_success_url())
def get_success_url(self):
return self.request.META.get('HTTP_REFERER') or reverse_lazy('device:details')
class AddNoteView(FormView):
form_class = AddNoteForm
def form_valid(self, form):
note_text = form.cleaned_data['note']
snapshot_uuid = form.cleaned_data['snapshot_uuid']
Note.objects.create(
snapshot_uuid=snapshot_uuid,
description=note_text,
user=self.request.user,
institution=self.request.user.institution,
)
message = _("<Created> Note: '{}'").format(note_text)
DeviceLog.objects.create(
snapshot_uuid=snapshot_uuid,
event=message,
user=self.request.user,
institution=self.request.user.institution,
)
messages.success(self.request, _("Note has been added"))
return super().form_valid(form)
def form_invalid(self, form):
messages.error(self.request, _("There was an error with your submission."))
return redirect(self.get_success_url())
def get_success_url(self):
return self.request.META.get('HTTP_REFERER') or reverse_lazy('device:details')
class UpdateNoteView(UpdateView):
model = Note
fields = ['description']
pk_url_kwarg = 'pk'
def form_valid(self, form):
old_description = self.get_object().description
new_description = self.object.description
snapshot_uuid = self.object.snapshot_uuid
if old_description != new_description:
message = _("<Updated> Note. Old Description: '{}'. New Description: '{}'").format(old_description, new_description)
DeviceLog.objects.create(
snapshot_uuid=snapshot_uuid,
event=message,
user=self.request.user,
institution=self.request.user.institution,
)
messages.success(self.request, "Note has been updated.")
return super().form_valid(form)
def form_invalid(self, form):
new_description = form.cleaned_data.get('description', '').strip()
if not new_description:
messages.error(self.request, _("Note cannot be empty."))
super().form_invalid(form)
return redirect(self.get_success_url())
def get_success_url(self):
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details'))
class DeleteNoteView(View):
model = Note
def post(self, request, *args, **kwargs):
self.pk = kwargs['pk']
referer = request.META.get('HTTP_REFERER')
if not referer:
raise Http404("No referer header found")
self.object = get_object_or_404(
self.model,
pk=self.pk,
institution=self.request.user.institution
)
description = self.object.description
snapshot_uuid= self.object.snapshot_uuid
if request.user != self.object.user and not request.user.is_admin:
messages.error(request, _("You do not have permission to delete this note."))
return redirect(referer)
message = _("<Deleted> Note. Description: '{}'. ").format(description)
DeviceLog.objects.create(
snapshot_uuid=snapshot_uuid,
event=message,
user=request.user,
institution=request.user.institution,
)
messages.warning(self.request, _("Note '{}' deleted successfully.").format(description))
self.object.delete()
return redirect(referer)
# from django.shortcuts import render

View file

@ -1,5 +0,0 @@
from django import forms
class OrderingStateForm(forms.Form):
ordering = forms.CharField()

View file

@ -1,233 +0,0 @@
{% extends "base.html" %}
{% load i18n django_bootstrap5 %}
{% block content %}
<div class="row">
<div class="col">
<h3>{{ subtitle }}</h3>
</div>
<div class="col text-end">
<button type="button" class="btn btn-green-admin" data-bs-toggle="modal" data-bs-target="#addStateModal">
{% trans "Add" %}
</button>
</div>
</div>
<div class="row mt-4">
<div class="col">
{% if state_definitions %}
<table class="table table-hover table-bordered align-middle">
<caption class="text-muted small">
{% trans 'Move and drag state definitions to reorder' %}
</caption>
<thead class="table-light">
<tr>
<th scope="col" width="1%" class="text-start">
</th>
<th scope="col" width="5%" class="text-center">
#</th>
<th scope="col">{% trans "State Definition" %}
</th>
<th scope="col" width="15%" class="text-center">{% trans "Actions" %}
</th>
</tr>
</thead>
<tbody id="sortable_list">
{% for state_definition in state_definitions %}
<tr
data-lookup="{{ state_definition.id }}"
style="cursor: grab;"
class="align-items-center">
<td class="">
<i class="bi bi-grip-vertical" aria-hidden="true">
</i>
</td>
<td class="text-center">
<strong>{{ state_definition.order }} </strong>
</td>
<td class="font-monospace">
{{ state_definition.state }}
</td>
<!-- action buttons -->
<td>
<div class="btn-group float-end">
<button
type="button"
class="btn btn-sm btn-outline-info d-flex align-items-center"
data-bs-toggle="modal" data-bs-target="#editStateModal{{ state_definition.id }}">
<i class="bi bi-pencil me-1"></i>
{% trans 'Edit' %}
</button>
<button
type="button" class="btn btn-sm btn-outline-danger d-flex align-items-center"
data-bs-toggle="modal"
data-bs-target="#deleteStateModal{{ state_definition.id }}" >
<i class="bi bi-trash me-1"></i>
{% trans 'Delete' %}
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<form id="orderingForm" method="post" action="{% url 'admin:update_state_order' %}">
{% csrf_token %}
<input type="hidden" id="orderingInput" name="ordering">
<button id="saveOrderBtn" class="btn btn-success mt-5 float-start collapse" >{% trans "Update Order" %}</button>
</form>
{% else %}
<div class="alert alert-primary text-center mt-5" role="alert">
{% trans "No states found on current organization" %}
</div>
{% endif %}
</div>
</div>
<!-- add state definition Modal -->
<div class="modal fade" id="addStateModal" tabindex="-1" aria-labelledby="addStateModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addStateModalLabel">{% trans "Add State Definition" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="{%url 'admin:add_state_definition'%}">
{% csrf_token %}
<div class="mb-3">
<label for="stateInput" class="form-label">{% trans "State" %}</label>
<input type="text" class="form-control" id="stateInput" name="state" maxlength="50" required>
<div class="form-text">{% trans "Maximum 50 characters." %}</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Add state definition" %}</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Edit State Definition Modals -->
{% for state_definition in state_definitions %}
<div class="modal fade" id="editStateModal{{ state_definition.id }}" tabindex="-1" aria-labelledby="editStateModalLabel{{ state_definition.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="{% url 'admin:edit_state_definition' state_definition.id %}">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title" id="editStateModalLabel{{ state_definition.id }}">
{% trans "Edit State Definition" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning text-center" role="alert">
{% trans "Existing devices with this state will not have their state names changed." %}
</div>
<div class="mb-3">
<label for="editStateInput{{ state_definition.id }}" class="form-label">{% trans "State" %}</label>
<input type="text" class="form-control" id="editStateInput{{ state_definition.id }}" name="state" maxlength="50" value="{{ state_definition.state }}" required>
<div class="form-text">{% trans "Maximum 50 characters." %}</div>
</div>
<p class="text-muted text-end">{% trans "Any changes in order will not be saved." %}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-green-admin">{% trans "Save Changes" %}</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
<!-- delete state definition Modal -->
{% for state_definition in state_definitions %}
<div class="modal fade" id="deleteStateModal{{ state_definition.id }}" tabindex="-1" aria-labelledby="deleteStateModalLabel{{ state_definition.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="deleteStateModalLabel{{ state_definition.id }}">
{% trans "Delete State Definition" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning text-center" role="alert">
{% trans "Devices with a State of this description will not have their State altered" %}
</div>
<div class="d-flex align-items-center border rounded p-3 mt-3">
<span class="badge bg-secondary me-3 display-6">{{ state_definition.order }}</span>
<div>
<p class="mb-0 fw-bold">{{ state_definition.state }}</p>
</div>
</div>
<p class="text-muted text-end mt-3">{% trans "Any changes in order will not be saved." %}</p>
</div>
<div class="modal-footer">
<form method="post" action="{% url 'admin:delete_state_definition' state_definition.pk %}">
{% csrf_token %}
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{% trans "Cancel" %}
</button>
<button type="submit" class="btn btn-danger">
{% trans "Delete" %}
</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
<script>
//following https://dev.to/nemecek_f/django-how-to-let-user-re-order-sort-table-of-content-with-drag-and-drop-3nlp
const saveOrderingButton = document.getElementById('saveOrderBtn');
const orderingForm = document.getElementById('orderingForm');
const formInput = orderingForm.querySelector('#orderingInput');
const sortable_table = document.getElementById('sortable_list');
const sortable = new Sortable(sortable_table, {
animation: 150,
swapThreshold: 0.10,
onChange: () => {
//TODO: change hide/show animation to a nicer one
const collapse = new bootstrap.Collapse(saveOrderingButton, {
toggle: false
});
collapse.show();
}
});
function saveOrdering() {
const rows = sortable_table.querySelectorAll('tr');
let ids = [];
for (let row of rows) {
ids.push(row.dataset.lookup);
}
formInput.value = ids.join(',');
orderingForm.submit();
}
saveOrderingButton.addEventListener('click', saveOrdering);
</script>
{% endblock %}

View file

@ -10,9 +10,4 @@ urlpatterns = [
path("users/edit/<int:pk>", views.EditUserView.as_view(), name="edit_user"),
path("users/delete/<int:pk>", views.DeleteUserView.as_view(), name="delete_user"),
path("institution/<int:pk>", views.InstitutionView.as_view(), name="institution"),
path("states/", views.StatesPanelView.as_view(), name="states_panel"),
path("states/add", views.AddStateDefinitionView.as_view(), name="add_state_definition"),
path("states/delete/<int:pk>", views.DeleteStateDefinitionView.as_view(), name='delete_state_definition'),
path("states/update_order/", views.UpdateStateOrderView.as_view(), name='update_state_order'),
path("states/edit/<int:pk>/", views.UpdateStateDefinitionView.as_view(), name='edit_state_definition'),
]

View file

@ -1,23 +1,16 @@
import logging
from smtplib import SMTPException
from django.contrib import messages
from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404, redirect, Http404
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic.base import TemplateView, ContextMixin
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
CreateView,
UpdateView,
DeleteView,
)
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from dashboard.mixins import DashboardView, Http403
from admin.forms import OrderingStateForm
from user.models import User, Institution
from admin.email import NotifyActivateUserByEmail
from action.models import StateDefinition
class AdminView(DashboardView):
@ -131,101 +124,3 @@ class InstitutionView(AdminView, UpdateView):
self.object = self.request.user.institution
kwargs = super().get_form_kwargs()
return kwargs
class StateDefinitionContextMixin(ContextMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
"state_definitions": StateDefinition.objects.filter(institution=self.request.user.institution).order_by('order'),
"help_text": _('State definitions are the custom finite states that a device can be in.'),
})
return context
class StatesPanelView(AdminView, StateDefinitionContextMixin, TemplateView):
template_name = "states_panel.html"
title = _("States Panel")
breadcrumb = _("admin / States Panel") + " /"
class AddStateDefinitionView(AdminView, StateDefinitionContextMixin, CreateView):
template_name = "states_panel.html"
title = _("New State Definition")
breadcrumb = "Admin / New state"
success_url = reverse_lazy('admin:states_panel')
model = StateDefinition
fields = ('state',)
def form_valid(self, form):
form.instance.institution = self.request.user.institution
form.instance.user = self.request.user
try:
response = super().form_valid(form)
messages.success(self.request, _("State definition successfully added."))
return response
except IntegrityError:
messages.error(self.request, _("State is already defined."))
return self.form_invalid(form)
def form_invalid(self, form):
return super().form_invalid(form)
class DeleteStateDefinitionView(AdminView, StateDefinitionContextMixin, SuccessMessageMixin, DeleteView):
model = StateDefinition
success_url = reverse_lazy('admin:states_panel')
def get_success_message(self, cleaned_data):
return f'State definition: {self.object.state}, has been deleted'
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
#only an admin of current institution can delete
if not object.institution == self.request.user.institution:
raise Http404
return super().delete(request, *args, **kwargs)
class UpdateStateOrderView(AdminView, TemplateView):
success_url = reverse_lazy('admin:states_panel')
def post(self, request, *args, **kwargs):
form = OrderingStateForm(request.POST)
if form.is_valid():
ordered_ids = form.cleaned_data["ordering"].split(',')
with transaction.atomic():
current_order = 1
_log = []
for lookup_id in ordered_ids:
state_definition = StateDefinition.objects.get(id=lookup_id)
state_definition.order = current_order
state_definition.save()
_log.append(f"{state_definition.state} (ID: {lookup_id} -> Order: {current_order})")
current_order += 1
messages.success(self.request, _("Order changed succesfuly."))
return redirect(self.success_url)
else:
return Http404
class UpdateStateDefinitionView(AdminView, UpdateView):
model = StateDefinition
template_name = 'states_panel.html'
fields = ['state']
pk_url_kwarg = 'pk'
def get_queryset(self):
return StateDefinition.objects.filter(institution=self.request.user.institution)
def get_success_url(self):
messages.success(self.request, _("State definition updated successfully."))
return reverse_lazy('admin:states_panel')
def form_valid(self, form):
return super().form_valid(form)

View file

@ -7,7 +7,7 @@ app_name = 'api'
urlpatterns = [
path('v1/snapshot/', views.NewSnapshotView.as_view(), name='new_snapshot'),
path('v1/property/<str:pk>/', views.AddPropertyView.as_view(), name='new_property'),
path('v1/annotation/<str:pk>/', views.AddAnnotationView.as_view(), name='new_annotation'),
path('v1/device/<str:pk>/', views.DetailsDeviceView.as_view(), name='device'),
path('v1/tokens/', views.TokenView.as_view(), name='tokens'),
path('v1/tokens/new', views.TokenNewView.as_view(), name='new_token'),

View file

@ -21,7 +21,7 @@ from django.views.generic.edit import (
from utils.save_snapshots import move_json, save_in_disk
from django.views.generic.edit import View
from dashboard.mixins import DashboardView
from evidence.models import SystemProperty, UserProperty
from evidence.models import Annotation
from evidence.parse_details import ParseSnapshot
from evidence.parse import Build
from device.models import Device
@ -94,11 +94,11 @@ class NewSnapshotView(ApiMixing):
logger.error("%s", txt)
return JsonResponse({'status': txt}, status=500)
exist_property = SystemProperty.objects.filter(
exist_annotation = Annotation.objects.filter(
uuid=ev_uuid
).first()
if exist_property:
if exist_annotation:
txt = "error: the snapshot {} exist".format(ev_uuid)
logger.warning("%s", txt)
return JsonResponse({'status': txt}, status=500)
@ -115,24 +115,25 @@ class NewSnapshotView(ApiMixing):
text = "fail: It is not possible to parse snapshot"
return JsonResponse({'status': text}, status=500)
prop = SystemProperty.objects.filter(
annotation = Annotation.objects.filter(
uuid=ev_uuid,
type=Annotation.Type.SYSTEM,
# TODO this is hardcoded, it should select the user preferred algorithm
key="hidalgo1",
owner=self.tk.owner.institution
).first()
if not prop:
logger.error("Error: No property for uuid: %s", ev_uuid)
if not annotation:
logger.error("Error: No annotation for uuid: %s", ev_uuid)
return JsonResponse({'status': 'fail'}, status=500)
url_args = reverse_lazy("device:details", args=(property.value,))
url_args = reverse_lazy("device:details", args=(annotation.value,))
url = request.build_absolute_uri(url_args)
response = {
"status": "success",
"dhid": property.value[:6].upper(),
"dhid": annotation.value[:6].upper(),
"url": url,
# TODO replace with public_url when available
"public_url": url
@ -258,21 +259,22 @@ class DetailsDeviceView(ApiMixing):
"components": snapshot.get("components"),
})
uuids = SystemProperty.objects.filter(
uuids = Annotation.objects.filter(
owner=self.tk.owner.institution,
value=self.pk
).values("uuid")
properties = UserProperty.objects.filter(
annotations = Annotation.objects.filter(
uuid__in=uuids,
owner=self.tk.owner.institution,
type = Annotation.Type.USER
).values_list("key", "value")
data.update({"properties": list(properties)})
data.update({"annotations": list(annotations)})
return data
class AddPropertyView(ApiMixing):
class AddAnnotationView(ApiMixing):
def post(self, request, *args, **kwargs):
response = self.auth()
@ -281,12 +283,13 @@ class AddPropertyView(ApiMixing):
self.pk = kwargs['pk']
institution = self.tk.owner.institution
self.property = SystemProperty.objects.filter(
self.annotation = Annotation.objects.filter(
owner=institution,
value=self.pk,
type=Annotation.Type.SYSTEM
).first()
if not self.property:
if not self.annotation:
return JsonResponse({}, status=404)
try:
@ -297,9 +300,10 @@ class AddPropertyView(ApiMixing):
logger.error("Invalid Snapshot of user %s", self.tk.owner)
return JsonResponse({'error': 'Invalid JSON'}, status=500)
UserProperty.objects.create(
uuid=self.property.uuid,
Annotation.objects.create(
uuid=self.annotation.uuid,
owner=self.tk.owner.institution,
type = Annotation.Type.USER,
key = key,
value = value
)

View file

@ -6,7 +6,7 @@ from django.core.exceptions import PermissionDenied
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.base import TemplateView
from device.models import Device
from evidence.models import SystemProperty
from evidence.models import Annotation
from lot.models import LotTag
@ -49,7 +49,7 @@ class DashboardView(LoginRequiredMixin):
dev_ids = self.request.session.pop("devices", [])
self._devices = []
for x in SystemProperty.objects.filter(value__in=dev_ids).filter(
for x in Annotation.objects.filter(value__in=dev_ids).filter(
owner=self.request.user.institution
).distinct():
self._devices.append(Device(id=x.value))

File diff suppressed because one or more lines are too long

View file

@ -18,7 +18,6 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href= "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet">
<script src="{% static 'js/Sortable.min.js' %}"></script>
<style>
.bd-placeholder-img {
@ -52,7 +51,7 @@
}
</style>
<!-- Custom styles for this template -->
<link href="{% static "/css/dashboard.css" %}" rel="stylesheet">
{% endblock %}
@ -82,26 +81,21 @@
<ul class="nav flex-column">
{% if user.is_admin %}
<li class="nav-item">
<a class="admin {% if path in 'panel users states_panel edit_user delete_user new_user institution' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_admin" aria-expanded="false" aria-controls="ul_admin" href="javascript:void()">
<a class="admin {% if path in 'panel users' %}active {% endif %}nav-link fw-bold" data-bs-toggle="collapse" data-bs-target="#ul_admin" aria-expanded="false" aria-controls="ul_admin" href="javascript:void()">
<i class="bi bi-person-fill-gear icon_sidebar"></i>
{% trans 'Admin' %}
</a>
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'panel institution users edit_user new_user delete_user states_panel' %}expanded{% else %}collapse{% endif %}" id="ul_admin" data-bs-parent="#sidebarMenu">
<ul class="flex-column mb-2 ul_sidebar accordion-collapse {% if path in 'panel users' %}expanded{% else %}collapse{% endif %}" id="ul_admin" data-bs-parent="#sidebarMenu">
<li class="nav-item">
<a class="nav-link{% if path in 'panel institution' %} active2{% endif %}" href="{% url 'admin:panel' %}">
<a class="nav-link{% if path == 'panel' %} active2{% endif %}" href="{% url 'admin:panel' %}">
{% trans 'Panel' %}
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if path in 'users edit_user new_user delete_user' %} active2{% endif %}" href="{% url 'admin:users' %}">
<a class="nav-link{% if path == 'users' %} active2{% endif %}" href="{% url 'admin:users' %}">
{% trans 'Users' %}
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if path == 'states_panel' %} active2{% endif %}" href="{% url 'admin:states_panel' %}">
{% trans 'States' %}
</a>
</li>
</ul>
</li>
{% endif %}
@ -184,15 +178,9 @@
{% endfor %}
{% endblock messages %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2">
<h1 class="h2">{{ title }}
{% if help_text %}
<span class="ms-1" data-bs-toggle="tooltip" data-bs-placement="right" title="{{ help_text }}">
<i class="fas fa-question-circle text-secondary h6 align-top"></i>
</span>
{% endif %}
</h1>
<h1 class="h2">{{ title }}</h1>
<form method="post" action="{% url 'dashboard:search' %}">
<form method="post" action="{% url 'dashboard:search' %}">
{% csrf_token %}
<div class="input-group rounded">
<input type="search" name="search" class="form-control rounded" placeholder="Search your device..." aria-label="Search" aria-describedby="search-addon" />
@ -201,9 +189,9 @@
</span>
</div>
</form>
</div>
<div class="row border-bottom mb-3">
<div class="col">
<small style="color:#899bbd"><i>{{ breadcrumb }}</i></small>
@ -233,13 +221,4 @@
{% block extrascript %}{% endblock %}
{% endblock %}
</body>
<script>
//If help_text is passed to the view as context, a hover-able help icon is displayed
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
</script>
</html>

View file

@ -20,9 +20,9 @@
{% trans 'Exports' %}
</a>
{% if lot %}
<a href="{% url 'lot:properties' object.id %}" type="button" class="btn btn-green-admin">
<a href="{% url 'lot:annotations' object.id %}" type="button" class="btn btn-green-admin">
<i class="bi bi-tag"></i>
{% trans 'properties' %}
{% trans 'Annotations' %}
</a>
{% endif %}
</div>

View file

@ -6,7 +6,7 @@ from django.shortcuts import Http404
from django.db.models import Q
from dashboard.mixins import InventaryMixin, DetailsMixin
from evidence.models import SystemProperty
from evidence.models import Annotation
from evidence.xapian import search
from device.models import Device
from lot.models import Lot
@ -74,7 +74,7 @@ class SearchView(InventaryMixin):
for x in matches:
# devices.append(self.get_annotations(x))
dev = self.get_properties(x)
dev = self.get_annotations(x)
if dev.id not in dev_id:
devices.append(dev)
dev_id.append(dev.id)
@ -83,14 +83,13 @@ class SearchView(InventaryMixin):
# TODO fix of pagination, the count is not correct
return devices, count
def get_properties(self, xp):
def get_annotations(self, xp):
snap = json.loads(xp.document.get_data())
if snap.get("credentialSubject"):
uuid = snap["credentialSubject"]["uuid"]
else:
uuid = snap["uuid"]
return Device.get_properties_from_uuid(uuid, self.request.user.institution)
return Device.get_annotation_from_uuid(uuid, self.request.user.institution)
def search_hids(self, query, offset, limit):
qry = Q()
@ -99,7 +98,8 @@ class SearchView(InventaryMixin):
if i:
qry |= Q(value__startswith=i)
chids = SystemProperty.objects.filter(
chids = Annotation.objects.filter(
type=Annotation.Type.SYSTEM,
owner=self.request.user.institution
).filter(
qry

View file

@ -1,5 +1,5 @@
from django import forms
from utils.device import create_property, create_doc, create_index
from utils.device import create_annotation, create_doc, create_index
from utils.save_snapshots import move_json, save_in_disk
@ -59,7 +59,7 @@ class BaseDeviceFormSet(forms.BaseFormSet):
path_name = save_in_disk(doc, self.user.institution.name, place="placeholder")
create_index(doc, self.user)
create_property(doc, user, commit=commit)
create_annotation(doc, user, commit=commit)
move_json(path_name, self.user.institution.name, place="placeholder")
return doc

View file

@ -1,9 +1,8 @@
from django.db import models, connection
from utils.constants import ALGOS
from evidence.models import SystemProperty, UserProperty, Evidence
from evidence.models import Annotation, Evidence
from lot.models import DeviceLot
from action.models import State
class Device:
@ -31,7 +30,7 @@ class Device:
self.shortid = self.pk[:6].upper()
self.algorithm = None
self.owner = None
self.properties = []
self.annotations = []
self.hids = []
self.uuids = []
self.evidences = []
@ -40,59 +39,61 @@ class Device:
self.get_last_evidence()
def initial(self):
self.get_properties()
self.get_annotations()
self.get_uuids()
self.get_hids()
self.get_evidences()
self.get_lots()
def get_properties(self):
if self.properties:
return self.properties
def get_annotations(self):
if self.annotations:
return self.annotations
self.properties = SystemProperty.objects.filter(
self.annotations = Annotation.objects.filter(
type=Annotation.Type.SYSTEM,
value=self.id
).order_by("-created")
if self.properties.count():
self.algorithm = self.properties[0].key
self.owner = self.properties[0].owner
if self.annotations.count():
self.algorithm = self.annotations[0].key
self.owner = self.annotations[0].owner
return self.properties
return self.annotations
def get_user_properties(self):
def get_user_annotations(self):
if not self.uuids:
self.get_uuids()
user_properties = UserProperty.objects.filter(
annotations = Annotation.objects.filter(
uuid__in=self.uuids,
owner=self.owner,
type=UserProperty.Type.USER,
type=Annotation.Type.USER
)
return user_properties
return annotations
def get_user_documents(self):
if not self.uuids:
self.get_uuids()
user_properties = UserProperty.objects.filter(
annotations = Annotation.objects.filter(
uuid__in=self.uuids,
owner=self.owner,
type=UserProperty.Type.DOCUMENT
type=Annotation.Type.DOCUMENT
)
return user_properties
return annotations
def get_uuids(self):
for a in self.get_properties():
for a in self.get_annotations():
if a.uuid not in self.uuids:
self.uuids.append(a.uuid)
def get_hids(self):
properties = self.get_properties()
annotations = self.get_annotations()
algos = list(ALGOS.keys())
algos.append('CUSTOM_ID')
self.hids = list(set(properties.filter(
self.hids = list(set(annotations.filter(
type=Annotation.Type.SYSTEM,
key__in=algos,
).values_list("value", flat=True)))
@ -110,12 +111,12 @@ class Device:
self.last_evidence = Evidence(self.uuid)
return
properties = self.get_properties()
if not properties.count():
annotations = self.get_annotations()
if not annotations.count():
return
prop = properties.first()
self.last_evidence = Evidence(prop.uuid)
annotation = annotations.first()
self.last_evidence = Evidence(annotation.uuid)
self.uuid = annotation.uuid
def is_eraseserver(self):
if not self.uuids:
@ -123,13 +124,13 @@ class Device:
if not self.uuids:
return False
prop = UserProperty.objects.filter(
annotation = Annotation.objects.filter(
uuid__in=self.uuids,
owner=self.owner,
type=UserProperty.Type.ERASE_SERVER
type=Annotation.Type.ERASE_SERVER
).first()
if prop:
if annotation:
return True
return False
@ -138,11 +139,6 @@ class Device:
return self.uuid
return self.uuids[0]
def get_current_state(self):
uuid = self.last_uuid
return State.objects.filter(snapshot_uuid=uuid).order_by('-date').first()
def get_lots(self):
self.lots = [
x.lot for x in DeviceLot.objects.filter(device_id=self.id)]
@ -151,7 +147,7 @@ class Device:
def get_unassigned(cls, institution, offset=0, limit=None):
sql = """
WITH RankedProperties AS (
WITH RankedAnnotations AS (
SELECT
t1.value,
t1.key,
@ -164,32 +160,34 @@ class Device:
ELSE 3
END,
t1.created DESC
) AS row_num
FROM evidence_systemproperty AS t1
) AS row_num
FROM evidence_annotation AS t1
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
WHERE t2.device_id IS NULL
AND t1.owner_id = {institution}
AND t1.type = {type}
)
SELECT DISTINCT
value
FROM
RankedProperties
RankedAnnotations
WHERE
row_num = 1
""".format(
institution=institution.id,
type=Annotation.Type.SYSTEM,
)
if limit:
sql += " limit {} offset {}".format(int(limit), int(offset))
sql += ";"
properties = []
annotations = []
with connection.cursor() as cursor:
cursor.execute(sql)
properties = cursor.fetchall()
annotations = cursor.fetchall()
devices = [cls(id=x[0]) for x in properties]
devices = [cls(id=x[0]) for x in annotations]
count = cls.get_unassigned_count(institution)
return devices, count
@ -197,7 +195,7 @@ class Device:
def get_unassigned_count(cls, institution):
sql = """
WITH RankedProperties AS (
WITH RankedAnnotations AS (
SELECT
t1.value,
t1.key,
@ -211,28 +209,30 @@ class Device:
END,
t1.created DESC
) AS row_num
FROM evidence_systemproperty AS t1
FROM evidence_annotation AS t1
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
WHERE t2.device_id IS NULL
AND t1.owner_id = {institution}
AND t1.type = {type}
)
SELECT
COUNT(DISTINCT value)
FROM
RankedProperties
RankedAnnotations
WHERE
row_num = 1
""".format(
institution=institution.id,
type=Annotation.Type.SYSTEM,
)
with connection.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchall()[0][0]
@classmethod
def get_properties_from_uuid(cls, uuid, institution):
def get_annotation_from_uuid(cls, uuid, institution):
sql = """
WITH RankedProperties AS (
WITH RankedAnnotations AS (
SELECT
t1.value,
t1.key,
@ -246,29 +246,31 @@ class Device:
END,
t1.created DESC
) AS row_num
FROM evidence_systemproperty AS t1
FROM evidence_annotation AS t1
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
WHERE t2.device_id IS NULL
AND t1.owner_id = {institution}
AND t1.type = {type}
AND t1.uuid = '{uuid}'
)
SELECT DISTINCT
value
FROM
RankedProperties
RankedAnnotations
WHERE
row_num = 1;
""".format(
uuid=uuid.replace("-", ""),
institution=institution.id,
type=Annotation.Type.SYSTEM,
)
properties = []
annotations = []
with connection.cursor() as cursor:
cursor.execute(sql)
properties = cursor.fetchall()
annotations = cursor.fetchall()
return cls(id=properties[0][0])
return cls(id=annotations[0][0])
@property
def is_websnapshot(self):

View file

@ -2,162 +2,11 @@
{% load i18n %}
{% block content %}
<div class="position-fixed" style="bottom: 2rem; right: 2rem; z-index: 9999; display: flex; gap: 0.5rem;">
<button class="btn btn-yellow d-flex align-items-center shadow" type="button"
data-bs-toggle="offcanvas" data-bs-target="#notesOffcanvas" aria-controls="notesOffcanvas"
data-bs-toggle="tooltip" data-bs-placement="left" title="{% trans 'View recent notes' %}">
<i class="bi bi-journal-text me-1"></i>
{% trans "Journal" %}
</button>
</div>
<!-- side panel for latest notes -->
<div class="offcanvas offcanvas-end" tabindex="-1" id="notesOffcanvas" aria-labelledby="notesOffcanvasLabel">
<div class="offcanvas-header">
<h5 id="notesOffcanvasLabel">{% trans "Latest Notes" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body" style="margin-bottom: 5rem;">
{% for note in device_notes|slice:":4" %}
<div class="card mb-3 shadow-sm">
<div class="card-body">
<div>
<small class="text-muted">
{{ note.date|timesince }} {% trans "ago" %}
</small>
{% if user == note.user or user.is_admin %}
<span class="badge bg-yellow text-dark ms-2">{% trans "Editable" %}</span>
</div>
<blockquote
class="blockquote mt-2 p-2 bg-light fst-italic"
contenteditable="true"
style="font-size: 1.2em!important"
data-note-id="{{ note.id }}"
title="{% trans 'Click to edit this note' %}"
oninput="toggleSaveLink(this)">
{% else %}
</div>
<blockquote style="font-size: 1.2em!important" class="blockquote mt-2 p-2 fst-italic">
{% endif %}
<p data-note-id="{{ note.id }}">
{{ note.description }}
</p>
<footer class="blockquote-footer text-end mt-2" contenteditable="false">
<small>{{ note.user.get_full_name|default:note.user.username }}</small>
</footer>
</blockquote>
{% if user == note.user or user.is_admin %}
<div class="d-flex justify-content-end align-items-center">
<!-- update note button -->
<form
id="updateNoteForm{{ note.id }}"
method="post"
action="{% url 'action:update_note' note.id %}"
class="d-inline"
>
{% csrf_token %}
<input type="hidden" name="description" id="descriptionInput{{ note.id }}" value="">
<a
type="submit"
id="saveLink{{ note.id }}"
class="text-muted disabled me-4 border border-light rounded"
style="pointer-events: none;"
title="{% trans 'Save changes' %}"
onclick="submitUpdatedNote('{{ note.id }}'); return false;"
>
<i class="fas fa-save px-1"></i>
</a>
</form>
<!-- delete note button -->
<button type="button" class="btn btn-link btn-outline-danger btn-sm text-danger" id="deleteIcon{{ note.id }}" title="{% trans 'Delete note' %}" data-bs-toggle="collapse" data-bs-target="#confirmDelete{{ note.id }}">
<i class="bi bi-trash"></i>
</button>
</div>
<form class="d-inline" method="post" action="{% url 'action:delete_note' note.id %}">
{% csrf_token %}
<div class="collapse mt-2" id="confirmDelete{{ note.id }}">
<div class="card card-body border border-danger text-center">
<p class="mb-2">{% trans 'Are you sure you want to delete this note?' %}</p>
<a
href="#"
class="btn btn-sm btn-outline-danger"
onclick="submitDeleteForm({{ note.id }}); return false;"
>
{% trans 'Confirm delete' %}
</a>
</div>
</div>
</form>
{% endif %}
</div>
</div>
{% empty %}
<p>{% trans "No notes available." %}</p>
{% endfor %}
</div>
</div>
<!-- Top bar buttons -->
<div class="row">
<div class="col">
<h3>{{ object.shortid }}</h3>
</div>
<div class="col text-end">
<div class="btn-group" role="group" aria-label="Actions">
<!-- change state button -->
{% if state_definitions %}
<div class="dropdown ms-2">
<a class="btn btn-green-admin dropdown-toggle" id="addStateDropdown" data-bs-toggle="dropdown" aria-expanded="false">
{% trans "Change state" %}
{% if device_states %}
({{ device_states.0.state }})
{% else %}
( {% trans "None" %} )
{% endif %}
</a>
<ul class="dropdown-menu" aria-labelledby="addStateDropdown" style="width: 100%;">
{% for state in state_definitions %}
<li style="width: 100%;">
<form id="changeStateForm{{ state.id }}" method="post" action="{% url 'action:change_state' %}">
{% csrf_token %}
<input type="hidden" name="previous_state" value="{{ device_states.0.state|default:"nil" }}">
<input type="hidden" name="snapshot_uuid" value="{{ object.last_uuid }}">
<input type="hidden" name="new_state" value="{{ state.state }}">
<a class="dropdown-item d-flex justify-content-between align-items-center" href="#" onclick="document.getElementById('changeStateForm{{ state.id }}').submit(); return false;">
<span class="font-monospace">{{ state.state }}</span>
<span class="badge bg-secondary rounded-pill-sm">{{ forloop.counter }}</span>
</a>
</form>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<button class="btn btn-green-admin" type="button" disabled>
<i class="bi bi-plus"></i> {% trans "Change state" %}
{% if device_states %}
({{ device_states.0.state }})
{% endif %}
</button>
{% endif %}
<!-- Add note button -->
<button class="btn btn-yellow ms-2" type="button" data-bs-toggle="modal" data-bs-target="#addNoteModal">
<i class="bi bi-sticky"></i> {% trans "Add a note" %}
</button>
<div class="row">
<div class="col">
<h3>{{ object.shortid }}</h3>
</div>
</div>
</div>
<div class="row">
<div class="col">
@ -166,10 +15,7 @@
<a href="#details" class="nav-link active" data-bs-toggle="tab" data-bs-target="#details">{% trans 'General details' %}</a>
</li>
<li class="nav-item">
<a href="#log" class="nav-link" data-bs-toggle="tab" data-bs-target="#log">{% trans 'Log' %}</a>
</li>
<li class="nav-item">
<a href="#user_properties" class="nav-link" data-bs-toggle="tab" data-bs-target="#user_properties">{% trans 'User properties' %}</a>
<a href="#annotations" class="nav-link" data-bs-toggle="tab" data-bs-target="#annotations">{% trans 'User annotations' %}</a>
</li>
<li class="nav-item">
<a href="#documents" class="nav-link" data-bs-toggle="tab" data-bs-target="#documents">{% trans 'Documents' %}</a>
@ -194,48 +40,227 @@
</ul>
</div>
</div>
<div class="tab-content pt-4">
{% include 'tabs/general_details.html' %}
<div class="tab-content pt-2">
<div class="tab-pane fade show active" id="details">
<h5 class="card-title">{% trans 'Details' %}</h5>
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label">Phid</div>
<div class="col-lg-9 col-md-8">{{ object.id }}</div>
</div>
{% include 'tabs/log.html' %}
{% include 'tabs/user_properties.html' %}
{% include 'tabs/documents.html' %}
{% include 'tabs/lots.html' %}
{% include 'tabs/components.html' %}
{% include 'tabs/evidences.html' %}
<!-- Add a note popup -->
<div class="modal fade" id="addNoteModal" tabindex="-1" aria-labelledby="addNoteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addNoteModalLabel">{% trans "Add a Note" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
{% if object.is_eraseserver %}
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label">
{% trans 'Is a erase server' %}
</div>
<div class="col-lg-9 col-md-8"></div>
</div>
<div class="modal-body">
<form method="post" action="{% url 'action:add_note' %}">
{% csrf_token %}
<div class="mb-3">
<input type="hidden" name="snapshot_uuid" value="{{ object.last_uuid }}">
<label for="noteDescription" class="form-label">{% trans "Note" %}</label>
<textarea class="form-control" id="noteDescription" name="note" placeholder="Max 250 characters" name="note" rows="3" required></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-green-admin">{% trans "Save Note" %}</button>
</div>
</form>
{% endif %}
<div class="row mb-1">
<div class="col-lg-3 col-md-4 label">Type</div>
<div class="col-lg-9 col-md-8">{{ object.type }}</div>
</div>
{% if object.is_websnapshot and object.last_user_evidence %}
{% for k, v in object.last_user_evidence %}
<div class="row mb-1">
<div class="col-lg-3 col-md-4 label">{{ k }}</div>
<div class="col-lg-9 col-md-8">{{ v|default:'' }}</div>
</div>
{% endfor %}
{% else %}
<div class="row mb-1">
<div class="col-lg-3 col-md-4 label">
{% trans 'Manufacturer' %}
</div>
<div class="col-lg-9 col-md-8">{{ object.manufacturer|default:'' }}</div>
</div>
<div class="row mb-1">
<div class="col-lg-3 col-md-4 label">
{% trans 'Model' %}
</div>
<div class="col-lg-9 col-md-8">{{ object.model|default:'' }}</div>
</div>
<div class="row mb-1">
<div class="col-lg-3 col-md-4 label">
{% trans 'Version' %}
</div>
<div class="col-lg-9 col-md-8">{{ object.version|default:'' }}</div>
</div>
<div class="row mb-1">
<div class="col-lg-3 col-md-4 label">
{% trans 'Serial Number' %}
</div>
<div class="col-lg-9 col-md-8">{{ object.serial_number|default:'' }}</div>
</div>
{% endif %}
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label">
{% trans 'Identifiers' %}
</div>
</div>
{% for chid in object.hids %}
<div class="row mb-3">
<div class="col">{{ chid|default:'' }}</div>
</div>
{% endfor %}
</div>
</div>
<div class="tab-pane fade" id="annotations">
<div class="btn-group mt-1 mb-3">
<a href="{% url 'device:add_annotation' object.pk %}" class="btn btn-primary">
<i class="bi bi-plus"></i>
{% trans 'Add new annotation' %}
</a>
</div>
<h5 class="card-title">{% trans 'Annotations' %}</h5>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">
{% trans 'Key' %}
</th>
<th scope="col">
{% trans 'Value' %}
</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD HH:mm">
{% trans 'Created on' %}
</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in object.get_user_annotations %}
<tr>
<td>{{ a.key }}</td>
<td>{{ a.value }}</td>
<td>{{ a.created }}</td>
<td></td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="tab-pane fade" id="documents">
<div class="btn-group mt-1 mb-3">
<a href="{% url 'device:add_document' object.pk %}" class="btn btn-primary">
<i class="bi bi-plus"></i>
{% trans 'Add new document' %}
</a>
</div>
<h5 class="card-title">{% trans 'Documents' %}</h5>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">
{% trans 'Key' %}
</th>
<th scope="col">
{% trans 'Value' %}
</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD HH:mm">
{% trans 'Created on' %}
</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in object.get_user_documents %}
<tr>
<td>{{ a.key }}</td>
<td>{{ a.value }}</td>
<td>{{ a.created }}</td>
<td></td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="tab-pane fade" id="lots">
{% for tag in lot_tags %}
<h5 class="card-title">{{ tag }}</h5>
{% for lot in object.lots %}
{% if lot.type == tag %}
<div class="row mb-3">
<div class="col">
<a href="{% url 'dashboard:lot' lot.id %}">{{ lot.name }}</a>
</div>
</div>
{% endif %}
{% endfor %}
{% endfor %}
</div>
<div class="tab-pane fade" id="components">
<h5 class="card-title">{% trans 'Components last evidence' %}</h5>
<div class="list-group col-6">
{% for c in object.components %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ c.type }}</h5>
<small class="text-muted">{{ evidence.created }}</small>
</div>
<p class="mb-1">
{% for k, v in c.items %}
{% if k not in 'actions,type' %}
{{ k }}: {{ v }}<br />
{% endif %}
{% endfor %}
</p>
</div>
{% endfor %}
</div>
</div>
<div class="tab-pane fade" id="evidences">
<h5 class="card-title">{% trans 'List of evidences' %}</h5>
<div class="list-group col-6">
{% for snap in object.evidences %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<small class="text-muted">{{ snap.created }}</small>
</div>
<p class="mb-1">
<a href="{% url 'evidence:details' snap.uuid %}">{{ snap.uuid }}</a>
</p>
</div>
{% endfor %}
</div>
</div>
{% if dpps %}
<div class="tab-pane fade" id="dpps">
<h5 class="card-title">{% trans 'List of dpps' %}</h5>
<div class="list-group col">
{% for d in dpps %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<small class="text-muted">{{ d.timestamp }}</small>
<span>{{ d.type }}</span>
</div>
<p class="mb-1">
<a href="{% url 'did:device_web' d.signature %}">{{ d.signature }}</a>
</p>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block extrascript %}
@ -256,27 +281,5 @@
}
}
})
//Enable save button on note if changes are made to it
function toggleSaveLink(blockquoteElem) {
const saveLink = document.getElementById("saveLink" + blockquoteElem.dataset.noteId);
saveLink.classList.remove("disabled", "text-muted", "border-light");
saveLink.classList.add("text-success", "border-success");
saveLink.style.pointerEvents = "auto";
}
//updates note-update-form with new value from blockquote
function submitUpdatedNote(noteId) {
const noteParagraph = document.querySelector('p[data-note-id="' + noteId + '"]');
const newText = noteParagraph.innerText.trim();
const descriptionField = document.getElementById('descriptionInput' + noteId);
descriptionField.value = newText;
document.getElementById('updateNoteForm' + noteId).submit();
}
//simpler are u sure? confirmation message
function submitDeleteForm(noteId) {
document.getElementById('confirmDelete' + noteId).closest('form').submit();
}
</script>
{% endblock %}

View file

@ -1,27 +0,0 @@
{% load i18n %}
<div class="tab-pane fade" id="components">
<h5 class="card-title">{% trans 'Components last evidence' %}
</h5>
<div class="list-group col-6">
{% for c in object.components %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ c.type }}
</h5>
<small class="text-muted">{{ evidence.created }}
</small>
</div>
<p class="mb-1">
{% for k, v in c.items %}
{% if k not in 'actions,type' %}
{{ k }}: {{ v }}
<br />
{% endif %}
{% endfor %}
</p>
</div>
{% endfor %}
</div>
</div>

View file

@ -1,49 +0,0 @@
{% load i18n %}
<div class="tab-pane fade" id="documents">
<div class="btn-group mt-1 mb-3">
<a href="{% url 'device:add_document' object.pk %}" class="btn btn-primary">
<i class="bi bi-plus">
</i>
{% trans 'Add new document' %}
</a>
</div>
<h5 class="card-title">{% trans 'Documents' %}
</h5>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">
{% trans 'Key' %}
</th>
<th scope="col">
{% trans 'Value' %}
</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD HH:mm">
{% trans 'Created on' %}
</th>
<th>
</th>
<th>
</th>
</tr>
</thead>
<tbody>
{% for a in object.get_user_documents %}
<tr>
<td>{{ a.key }}
</td>
<td>{{ a.value }}
</td>
<td>{{ a.created }}
</td>
<td>
</td>
<td>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View file

@ -1,19 +0,0 @@
{% load i18n %}
<div class="tab-pane fade" id="evidences">
<h5 class="card-title">{% trans 'List of evidences' %}</h5>
<div class="list-group col-6">
{% for snap in object.evidences %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<small class="text-muted">{{ snap.created }}</small>
</div>
<p class="mb-1">
<a href="{% url 'evidence:details' snap.uuid %}">{{ snap.uuid }}</a>
</p>
</div>
{% endfor %}
</div>
</div>
</div>

View file

@ -1,81 +0,0 @@
{% load i18n %}
<!-- Device Details -->
<div class="tab-pane fade show active" id="details">
<h5 class="card-title">{% trans 'Details' %}
</h5>
<hr>
<div class="row mb-3">
<div class="col-sm-4 text-muted fw-bold">{% trans 'Phid' %}
</div>
<div class="col-sm-8">{{ object.id }}
</div>
</div>
{% if object.is_eraseserver %}
<div class="row mb-3">
<div class="col-sm-4 text-muted fw-bold">{% trans 'Is an erase server' %}
</div>
<div class="col-sm-8">{% trans 'Yes' %}
</div>
</div>
{% endif %}
<div class="row mb-3">
<div class="col-sm-4 text-muted fw-bold">{% trans 'Type' %}
</div>
<div class="col-sm-8">{{ object.type }}
</div>
</div>
{% if object.is_websnapshot and object.last_user_evidence %}
{% for k, v in object.last_user_evidence.items %}
<div class="row mb-3">
<div class="col-sm-4 text-muted fw-bold">{{ k }}
</div>
<div class="col-sm-8">{{ v|default:'' }}
</div>
</div>
{% endfor %}
{% else %}
<div class="row mb-3">
<div class="col-sm-4 text-muted fw-bold">{% trans 'Manufacturer' %}
</div>
<div class="col-sm-8">{{ object.manufacturer|default:'' }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4 text-muted fw-bold">{% trans 'Model' %}
</div>
<div class="col-sm-8">{{ object.model|default:'' }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4 text-muted fw-bold">
{% trans 'Version' %}
</div>
<div class="col-sm-8">{{ object.version|default:'' }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-4 text-muted fw-bold">{% trans 'Serial Number' %}
</div>
<div class="col-sm-8">{{ object.serial_number|default:'' }}
</div>
</div>
{% endif %}
<div class="row mb-3">
<div class="col-sm-4 text-muted fw-bold">{% trans 'Identifiers' %}
</div>
<div class="col-sm-8">
{% for chid in object.hids %}
<div>{{ chid|default:'' }}
</div>
{% endfor %}
</div>
</div>
</div>

View file

@ -1,28 +0,0 @@
{% load i18n %}
<div class="tab-pane fade" id="log">
<div class="table-responsive">
<table class="table table-striped table-hover table-bordered bg-gradient">
<thead >
<tr>
<th scope="col">{% trans 'Date' %}</th>
<th scope="col">{% trans 'Event' %}</th>
<th scope="col">{% trans 'User' %}</th>
</tr>
</thead>
<tbody>
{% for log in device_logs %}
<tr>
<td width="13%">{{ log.date|date:"M j, Y, H:i" }}</td>
<td class="fst-italic">{{ log.event }}</td>
<td>{{ log.user.get_full_name|default:log.user.username }}</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center">{% trans 'No logs recorded.' %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View file

@ -1,19 +0,0 @@
{% load i18n %}
<div class="tab-pane fade" id="lots">
{% for tag in lot_tags %}
<h5 class="card-title">{{ tag }}
</h5>
{% for lot in object.lots %}
{% if lot.type == tag %}
<div class="row mb-3">
<div class="col">
<a href="{% url 'dashboard:lot' lot.id %}">{{ lot.name }}
</a>
</div>
</div>
{% endif %}
{% endfor %}
{% endfor %}
</div>

View file

@ -1,131 +0,0 @@
{% load i18n %}
<div class="tab-pane fade" id="user_properties">
<div class="d-flex justify-content-end mt-1 mb-3">
<a href="{% url 'device:add_user_property' object.pk %}"
class="btn btn-green-admin d-flex align-items-center">
<i class="bi bi-plus me-1"></i>
{% trans 'New user property' %}
</a>
</div>
<h5 class="card-title">{% trans 'User properties' %}</h5>
<table class="table table-hover table-bordered table-responsive align-middle">
<thead class="table-light">
<tr>
<th scope="col">{% trans 'Key' %}</th>
<th scope="col">{% trans 'Value' %}</th>
<th scope="col" data-type="date" class="text-end" data-format="YYYY-MM-DD HH:mm">{% trans 'Created on' %}</th>
<th scope="col" width="5%" class="text-end" title="{% trans 'Actions' %}"></th>
</tr>
</thead>
<tbody>
{% for a in object.get_user_properties %}
<tr>
<td>{{ a.key }}
</td>
<td>{{ a.value }}
</td>
<td class="text-end">{{ a.created }}
</td>
<td>
<div class="btn-group ">
<button
type="button"
class="btn btn-sm btn-outline-info d-flex align-items-center" data-bs-toggle="modal"
data-bs-target="#editModal{{ a.id }}" >
<i class="bi bi-pencil me-1"></i>
{% trans 'Edit' %}
</button>
<button
type="button"
class="btn btn-sm btn-outline-danger d-flex align-items-center"
data-bs-toggle="modal" data-bs-target="#deleteModal{{ a.id }}">
<i class="bi bi-trash me-1"></i>
{% trans 'Delete' %}
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- pop up modal for delete confirmation -->
{% for a in object.get_user_properties %}
<div class="modal fade" id="deleteModal{{ a.id }}" tabindex="-1" aria-labelledby="deleteModalLabel{{ a.id }}" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel{{ a.id }}">{% trans "Confirm Deletion" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
</button>
</div>
<div class="modal-body">
<p>
<strong>{% trans "Key:" %}
</strong> {{ a.key }}
</p>
<p>
<strong>{% trans "Value:" %}
</strong> {{ a.value }}
</p>
<p>
<strong>{% trans "Created on:" %}
</strong> {{ a.created }}
</p>
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}
</button>
<form method="post" action="{% url 'device:delete_user_property' a.id %}">
{% csrf_token %}
<button type="submit" class="btn btn-danger">{% trans "Delete" %}
</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- popup modals for edit button -->
{% for a in object.get_user_properties %}
<div class="modal fade" id="editModal{{ a.id }}" tabindex="-1" aria-labelledby="editModalLabel{{ a.id }}" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel{{ a.id }}">{% trans "Edit User Property" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
</button>
</div>
<div class="modal-body">
<form id="editForm{{ a.id }}" method="post" action="{% url 'device:update_user_property' a.id %}">
{% csrf_token %}
<div class="mb-3">
<label for="key" class="form-label">{% trans "Key" %}
</label>
<input type="text" class="form-control" id="key" name="key" value="{{ a.key }}">
</div>
<div class="mb-3">
<label for="value" class="form-label">{% trans "Value" %}
</label>
<input type="text" class="form-control" id="value" name="value" value="{{ a.value }}">
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}
</button>
<button type="submit" class="btn btn-primary">{% trans "Save changes" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endfor %}

View file

@ -7,9 +7,7 @@ urlpatterns = [
path("add/", views.NewDeviceView.as_view(), name="add"),
path("edit/<str:pk>/", views.EditDeviceView.as_view(), name="edit"),
path("<str:pk>/", views.DetailsView.as_view(), name="details"),
path("<str:pk>/user_property/add", views.AddUserPropertyView.as_view(), name="add_user_property"),
path("user_property/<int:pk>/delete", views.DeleteUserPropertyView.as_view(), name="delete_user_property"),
path("user_property/<int:pk>/update", views.UpdateUserPropertyView.as_view(), name="update_user_property"),
path("<str:pk>/annotation/add", views.AddAnnotationView.as_view(), name="add_annotation"),
path("<str:pk>/document/add", views.AddDocumentView.as_view(), name="add_document"),
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web"),

View file

@ -1,21 +1,16 @@
import json
import logging
from django.http import JsonResponse
from django.conf import settings
from django.urls import reverse_lazy
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, Http404
from django.shortcuts import get_object_or_404, Http404
from django.utils.translation import gettext_lazy as _
from django.views.generic.edit import (
CreateView,
UpdateView,
FormView,
DeleteView,
)
from django.views.generic.base import TemplateView
from action.models import StateDefinition, State, DeviceLog, Note
from dashboard.mixins import DashboardView, Http403
from evidence.models import UserProperty, SystemProperty
from evidence.models import Annotation
from lot.models import LotTag
from device.models import Device
from device.forms import DeviceFormSet
@ -75,7 +70,7 @@ class EditDeviceView(DashboardView, UpdateView):
title = _("Update Device")
breadcrumb = "Device / Update Device"
success_url = reverse_lazy('dashboard:unassigned_devices')
model = SystemProperty
model = Annotation
def get_form_kwargs(self):
pk = self.kwargs.get('pk')
@ -93,7 +88,7 @@ class DetailsView(DashboardView, TemplateView):
template_name = "details.html"
title = _("Device")
breadcrumb = "Device / Details"
model = SystemProperty
model = Annotation
def get(self, request, *args, **kwargs):
self.pk = kwargs['pk']
@ -115,20 +110,11 @@ class DetailsView(DashboardView, TemplateView):
uuid__in=self.object.uuids,
type=PROOF_TYPE["IssueDPP"]
)
last_evidence= self.object.get_last_evidence(),
uuid=self.object.last_uuid()
state_definitions = StateDefinition.objects.filter(
institution=self.request.user.institution
).order_by('order')
context.update({
'object': self.object,
'snapshot': last_evidence,
'snapshot': self.object.get_last_evidence(),
'lot_tags': lot_tags,
'dpps': dpps,
"state_definitions": state_definitions,
"device_states": State.objects.filter(snapshot_uuid=uuid).order_by('-date'),
"device_logs": DeviceLog.objects.filter(snapshot_uuid=uuid).order_by('-date'),
"device_notes": Note.objects.filter(snapshot_uuid=uuid).order_by('-date'),
})
return context
@ -189,136 +175,65 @@ class PublicDeviceWebView(TemplateView):
return JsonResponse(device_data)
class AddUserPropertyView(DashboardView, CreateView):
template_name = "new_user_property.html"
title = _("New User Property")
breadcrumb = "Device / New Property"
model = UserProperty
fields = ("key", "value")
def form_valid(self, form):
form.instance.owner = self.request.user.institution
form.instance.user = self.request.user
form.instance.uuid = self.property.uuid
form.instance.type = UserProperty.Type.USER
message = _("<Created> UserProperty: {}: {}".format(form.instance.key, form.instance.value))
DeviceLog.objects.create(
snapshot_uuid=form.instance.uuid,
event=message,
user=self.request.user,
institution=self.request.user.institution
)
messages.success(self.request, _("User property successfully added."))
response = super().form_valid(form)
return response
def get_form_kwargs(self):
pk = self.kwargs.get('pk')
institution = self.request.user.institution
self.property = get_object_or_404(SystemProperty, owner=institution, value=pk)
return super().get_form_kwargs()
def get_success_url(self):
return reverse_lazy('device:details', args=[self.kwargs.get('pk')])
class UpdateUserPropertyView(DashboardView, UpdateView):
template_name = "new_user_property.html"
title = _("Update User Property")
breadcrumb = "Device / Update Property"
model = UserProperty
fields = ("key", "value")
def get_queryset(self):
pk = self.kwargs.get('pk')
institution = self.request.user.institution
return UserProperty.objects.filter(pk=pk, owner=institution)
def form_valid(self, form):
old_instance = self.get_object()
old_key = old_instance.key
old_value = old_instance.value
form.instance.owner = self.request.user.institution
form.instance.user = self.request.user
form.instance.type = UserProperty.Type.USER
new_key = form.cleaned_data['key']
new_value = form.cleaned_data['value']
message = _("<Updated> UserProperty: {}: {} to {}: {}".format(old_key, old_value, new_key, new_value))
DeviceLog.objects.create(
snapshot_uuid=form.instance.uuid,
event=message,
user=self.request.user,
institution=self.request.user.institution
)
messages.success(self.request, _("User property updated successfully."))
return super().form_valid(form)
def get_success_url(self):
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details', args=[self.object.pk]))
class DeleteUserPropertyView(DashboardView, DeleteView):
model = UserProperty
def get_queryset(self):
return UserProperty.objects.filter(owner=self.request.user.institution)
#using post() method because delete() method from DeleteView has some issues with messages framework
def post(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.delete()
message = _("<Deleted> User Property: {}:{}".format(self.object.key, self.object.value ))
DeviceLog.objects.create(
snapshot_uuid=self.object.uuid,
event=message,
user=self.request.user,
institution=self.request.user.institution
)
messages.info(self.request, _("User property deleted successfully."))
return self.handle_success()
def handle_success(self):
return redirect(self.get_success_url())
def get_success_url(self):
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details', args=[self.object.pk]))
class AddDocumentView(DashboardView, CreateView):
template_name = "new_user_property.html"
title = _("New Document")
breadcrumb = "Device / New document"
class AddAnnotationView(DashboardView, CreateView):
template_name = "new_annotation.html"
title = _("New annotation")
breadcrumb = "Device / New annotation"
success_url = reverse_lazy('dashboard:unassigned_devices')
model = UserProperty
model = Annotation
fields = ("key", "value")
def form_valid(self, form):
form.instance.owner = self.request.user.institution
form.instance.user = self.request.user
form.instance.uuid = self.property.uuid
form.instance.type = UserProperty.Type.DOCUMENT
form.instance.uuid = self.annotation.uuid
form.instance.type = Annotation.Type.USER
response = super().form_valid(form)
return response
def get_form_kwargs(self):
pk = self.kwargs.get('pk')
institution = self.request.user.institution
self.property = SystemProperty.objects.filter(
self.annotation = Annotation.objects.filter(
owner=institution,
value=pk,
type=Annotation.Type.SYSTEM
).first()
if not self.property:
if not self.annotation:
raise Http404
self.success_url = reverse_lazy('device:details', args=[pk])
kwargs = super().get_form_kwargs()
return kwargs
class AddDocumentView(DashboardView, CreateView):
template_name = "new_annotation.html"
title = _("New Document")
breadcrumb = "Device / New document"
success_url = reverse_lazy('dashboard:unassigned_devices')
model = Annotation
fields = ("key", "value")
def form_valid(self, form):
form.instance.owner = self.request.user.institution
form.instance.user = self.request.user
form.instance.uuid = self.annotation.uuid
form.instance.type = Annotation.Type.DOCUMENT
response = super().form_valid(form)
return response
def get_form_kwargs(self):
pk = self.kwargs.get('pk')
institution = self.request.user.institution
self.annotation = Annotation.objects.filter(
owner=institution,
value=pk,
type=Annotation.Type.SYSTEM
).first()
if not self.annotation:
raise Http404
self.success_url = reverse_lazy('device:details', args=[pk])

View file

@ -65,7 +65,6 @@ ENABLE_EMAIL = config("ENABLE_EMAIL", default=True, cast=bool)
EVIDENCES_DIR = config("EVIDENCES_DIR", default=os.path.join(BASE_DIR, "db"))
# Application definition
INSTALLED_APPS = [
@ -216,10 +215,6 @@ LOGGING = {
'()': CustomFormatter,
'format': '%(levelname)s %(asctime)s %(message)s'
},
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
},
"handlers": {
"console": {
@ -242,7 +237,7 @@ LOGGING = {
"handlers": ["console"],
"level": "ERROR",
"propagate": False,
},
}
}
}

View file

@ -22,7 +22,6 @@ urlpatterns = [
path("", include("login.urls")),
path("dashboard/", include("dashboard.urls")),
path("evidence/", include("evidence.urls")),
path('action/', include('action.urls')),
path("device/", include("device.urls")),
path("admin/", include("admin.urls")),
path("user/", include("user.urls")),

View file

@ -1,17 +1,55 @@
services:
devicehub-django:
init: true
image: farga.pangea.org/ereuse/devicehub-django/latest
build:
context: .
dockerfile: docker/devicehub-django.Dockerfile
environment:
- DEBUG=${DEBUG:-false}
- DOMAIN=${DOMAIN:-localhost}
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-$DOMAIN}
- DOMAIN=${DEVICEHUB_DOMAIN:-localhost}
- PORT=${DEVICEHUB_PORT:-8000}
- ALLOWED_HOSTS=${DEVICEHUB_ALLOWED_HOSTS:-$DEVICEHUB_DOMAIN}
- DEMO=${DEMO:-false}
- PREDEFINED_TOKEN=${PREDEFINED_TOKEN:-}
- DPP=${DPP:-false}
# TODO manage volumes dev vs prod
volumes:
- .:/opt/devicehub-django
ports:
- 8000:8000
- ${DEVICEHUB_PORT:-8000}:${DEVICEHUB_PORT:-8000}
# TODO add database service for idhub, meanwhile sqlite
idhub:
# https://docs.docker.com/compose/how-tos/profiles/
profiles: [idhub]
init: true
image: farga.pangea.org/ereuse/idhub/latest
environment:
- DOMAIN=${IDHUB_DOMAIN:-localhost}
- ALLOWED_HOSTS=${IDHUB_ALLOWED_HOSTS:-$IDHUB_DOMAIN}
- DEBUG=true
- INITIAL_ADMIN_EMAIL=${IDHUB_ADMIN_EMAIL}
- INITIAL_ADMIN_PASSWORD=${IDHUB_ADMIN_PASSWD}
- CREATE_TEST_USERS=true
- ENABLE_EMAIL=${IDHUB_ENABLE_EMAIL:-true}
- ENABLE_2FACTOR_AUTH=${IDHUB_ENABLE_2FACTOR_AUTH:-true}
- ENABLE_DOMAIN_CHECKER=${IDHUB_ENABLE_DOMAIN_CHECKER:-true}
- SECRET_KEY=${IDHUB_SECRET_KEY:-publicsecretisnotsecureVtmKBfxpVV47PpBCF2Nzz2H6qnbd}
- STATIC_ROOT=${IDHUB_STATIC_ROOT:-/static/}
- MEDIA_ROOT=${IDHUB_MEDIA_ROOT:-/media/}
- PORT=${IDHUB_PORT:-9001}
- DEFAULT_FROM_EMAIL=${IDHUB_DEFAULT_FROM_EMAIL}
- EMAIL_HOST=${IDHUB_EMAIL_HOST}
- EMAIL_HOST_USER=${IDHUB_EMAIL_HOST_USER}
- EMAIL_HOST_PASSWORD=${IDHUB_EMAIL_HOST_PASSWORD}
- EMAIL_PORT=${IDHUB_EMAIL_PORT}
- EMAIL_USE_TLS=${IDHUB_EMAIL_USE_TLS}
- EMAIL_BACKEND=${IDHUB_EMAIL_BACKEND}
- SUPPORTED_CREDENTIALS=['DeviceSnapshotV1']
- SYNC_ORG_DEV=${IDHUB_SYNC_ORG_DEV}
ports:
- 9001:9001
# TODO add database service for idhub, meanwhile sqlite

View file

@ -19,12 +19,21 @@ main() {
cp -v .env.example .env
echo "WARNING: .env was not there, .env.example was copied, this only happens once"
fi
# load vars
. ./.env
if [ "${IDHUB_ENABLED:-}" = 'true' ]; then
export COMPOSE_PROFILES='idhub'
fi
# remove old database
rm -vfr ./db/*
# deactivate configured flag
rm -vfr ./already_configured
docker compose down -v
docker compose build
if [ "${DEV_DOCKER_ALWAYS_BUILD:-}" = 'true' ]; then
docker compose build
fi
docker compose up ${detach_arg:-}
}

View file

@ -37,6 +37,7 @@ RUN pip install -i https://test.pypi.org/simple/ ereuseapitest==0.0.14
# Set PYTHONPATH to include the directory with the xapian module
ENV PYTHONPATH="${PYTHONPATH}:/usr/lib/python3/dist-packages"
COPY . .
COPY docker/devicehub-django.entrypoint.sh /
RUN chown -R app:app /opt/devicehub-django

View file

@ -4,13 +4,12 @@ import pandas as pd
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from utils.device import create_property, create_doc, create_index
from utils.device import create_annotation, create_doc, create_index
from utils.forms import MultipleFileField
from device.models import Device
from evidence.parse import Build
from evidence.models import SystemProperty, UserProperty
from evidence.models import Annotation
from utils.save_snapshots import move_json, save_in_disk
from action.models import DeviceLog
class UploadForm(forms.Form):
@ -30,12 +29,13 @@ class UploadForm(forms.Form):
try:
file_json = json.loads(file_data)
snap = Build(file_json, None, check=True)
exists_property = SystemProperty.objects.filter(
build = Build
snap = build(file_json, None, check=True)
exist_annotation = Annotation.objects.filter(
uuid=snap.uuid
).first()
if exists_property:
if exist_annotation:
raise ValidationError(
_("The snapshot already exists"),
code="duplicate_snapshot",
@ -58,7 +58,9 @@ class UploadForm(forms.Form):
for ev in self.evidences:
path_name = save_in_disk(ev[1], user.institution.name)
Build(ev[1], user)
build = Build
file_json = ev[1]
build(file_json, user)
move_json(path_name, user.institution.name)
@ -69,8 +71,9 @@ class UserTagForm(forms.Form):
self.pk = None
self.uuid = kwargs.pop('uuid', None)
self.user = kwargs.pop('user')
instance = SystemProperty.objects.filter(
instance = Annotation.objects.filter(
uuid=self.uuid,
type=Annotation.Type.SYSTEM,
key='CUSTOM_ID',
owner=self.user.institution
).first()
@ -86,8 +89,9 @@ class UserTagForm(forms.Form):
if not data:
return False
self.tag = data
self.instance = SystemProperty.objects.filter(
self.instance = Annotation.objects.filter(
uuid=self.uuid,
type=Annotation.Type.SYSTEM,
key='CUSTOM_ID',
owner=self.user.institution
).first()
@ -99,31 +103,20 @@ class UserTagForm(forms.Form):
return
if self.instance:
old_value = self.instance.value
if not self.tag:
message =_("<Deleted> Evidence Tag. Old Value: '{}'").format(old_value)
self.instance.delete()
else:
self.instance.value = self.tag
self.instance.save()
if old_value != self.tag:
message=_("<Updated> Evidence Tag. Old Value: '{}'. New Value: '{}'").format(old_value, self.tag)
else:
message =_("<Created> Evidence Tag. Value: '{}'").format(self.tag)
SystemProperty.objects.create(
uuid=self.uuid,
key='CUSTOM_ID',
value=self.tag,
owner=self.user.institution,
user=self.user
)
DeviceLog.objects.create(
snapshot_uuid=self.uuid,
event= message,
user=self.user,
institution=self.user.institution
)
self.instance.value = self.tag
self.instance.save()
return
Annotation.objects.create(
uuid=self.uuid,
type=Annotation.Type.SYSTEM,
key='CUSTOM_ID',
value=self.tag,
owner=self.user.institution,
user=self.user
)
class ImportForm(forms.Form):
@ -174,8 +167,8 @@ class ImportForm(forms.Form):
table = []
for row in self.rows:
doc = create_doc(row)
property = create_property(doc, self.user)
table.append((doc, property))
annotation = create_annotation(doc, self.user)
table.append((doc, annotation))
if commit:
for doc, cred in table:
@ -196,9 +189,9 @@ class EraseServerForm(forms.Form):
self.pk = None
self.uuid = kwargs.pop('uuid', None)
self.user = kwargs.pop('user')
instance = UserProperty.objects.filter(
instance = Annotation.objects.filter(
uuid=self.uuid,
type=UserProperty.Type.ERASE_SERVER,
type=Annotation.Type.ERASE_SERVER,
key='ERASE_SERVER',
owner=self.user.institution
).first()
@ -211,9 +204,9 @@ class EraseServerForm(forms.Form):
def clean(self):
self.erase_server = self.cleaned_data.get('erase_server', False)
self.instance = UserProperty.objects.filter(
self.instance = Annotation.objects.filter(
uuid=self.uuid,
type=UserProperty.Type.ERASE_SERVER,
type=Annotation.Type.ERASE_SERVER,
key='ERASE_SERVER',
owner=self.user.institution
).first()
@ -232,9 +225,9 @@ class EraseServerForm(forms.Form):
if self.instance:
return
UserProperty.objects.create(
Annotation.objects.create(
uuid=self.uuid,
type=UserProperty.Type.ERASE_SERVER,
type=Annotation.Type.ERASE_SERVER,
key='ERASE_SERVER',
value=self.erase_server,
owner=self.user.institution,

69
evidence/legacy_parse.py Normal file
View file

@ -0,0 +1,69 @@
import json
import logging
from dmidecode import DMIParse
from json_repair import repair_json
from evidence.mixin_parse import BuildMix
from evidence.legacy_parse_details import get_lshw_child, ParseSnapshot
from utils.constants import CHASSIS_DH
logger = logging.getLogger('django')
def get_mac(lshw):
try:
if type(lshw) is dict:
hw = lshw
else:
hw = json.loads(lshw)
except json.decoder.JSONDecodeError:
hw = json.loads(repair_json(lshw))
nets = []
get_lshw_child(hw, nets, 'network')
nets_sorted = sorted(nets, key=lambda x: x['businfo'])
if nets_sorted:
mac = nets_sorted[0]['serial']
logger.debug("The snapshot has the following MAC: %s" , mac)
return mac
class Build(BuildMix):
# This parse is for get info from snapshots created with
# workbench-script but builded for send to devicehub-teal
def get_details(self):
dmidecode_raw = self.json["data"]["dmidecode"]
self.dmi = DMIParse(dmidecode_raw)
self.manufacturer = self.dmi.manufacturer().strip()
self.model = self.dmi.model().strip()
self.chassis = self.get_chassis_dh()
self.serial_number = self.dmi.serial_number()
self.sku = self.get_sku()
self.typ = self.chassis
self.version = self.get_version()
def get_chassis_dh(self):
chassis = self.get_chassis()
lower_type = chassis.lower()
for k, v in CHASSIS_DH.items():
if lower_type in v:
return k
return self.default
def get_sku(self):
return self.dmi.get("System")[0].get("SKU Number", "n/a").strip()
def get_chassis(self):
return self.dmi.get("Chassis")[0].get("Type", '_virtual') #
def get_version(self):
return self.dmi.get("System")[0].get("Verson", '_virtual')
def _get_components(self):
data = ParseSnapshot(self.json)
self.components = data.components

View file

@ -0,0 +1,503 @@
import json
import logging
import numpy as np
from datetime import datetime
from dmidecode import DMIParse
from json_repair import repair_json
from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE
logger = logging.getLogger('django')
def get_lshw_child(child, nets, component):
try:
if child.get('id') == component:
nets.append(child)
if child.get('children'):
[get_lshw_child(x, nets, component) for x in child['children']]
except Exception:
return []
class ParseSnapshot:
def __init__(self, snapshot, default="n/a"):
self.default = default
self.dmidecode_raw = snapshot["data"].get("dmidecode", "{}")
self.smart_raw = snapshot["data"].get("disks", [])
self.hwinfo_raw = snapshot["data"].get("hwinfo", "")
self.lshw_raw = snapshot["data"].get("lshw", {}) or {}
self.lscpi_raw = snapshot["data"].get("lspci", "")
self.device = {"actions": []}
self.components = []
self.monitors = []
self.dmi = DMIParse(self.dmidecode_raw)
self.smart = self.loads(self.smart_raw)
self.lshw = self.loads(self.lshw_raw)
self.hwinfo = self.parse_hwinfo()
self.set_computer()
self.get_hwinfo_monitors()
self.set_components()
self.snapshot_json = {
"type": "Snapshot",
"device": self.device,
"software": snapshot["software"],
"components": self.components,
"uuid": snapshot['uuid'],
"version": snapshot['version'],
"endTime": snapshot["timestamp"],
"elapsed": 1,
}
def set_computer(self):
self.device['manufacturer'] = self.dmi.manufacturer().strip()
self.device['model'] = self.dmi.model().strip()
self.device['serialNumber'] = self.dmi.serial_number()
self.device['type'] = self.get_type()
self.device['sku'] = self.get_sku()
self.device['version'] = self.get_version()
self.device['system_uuid'] = self.get_uuid()
self.device['family'] = self.get_family()
self.device['chassis'] = self.get_chassis_dh()
def set_components(self):
self.get_cpu()
self.get_ram()
self.get_mother_board()
self.get_graphic()
self.get_data_storage()
self.get_display()
self.get_sound_card()
self.get_networks()
def get_cpu(self):
for cpu in self.dmi.get('Processor'):
serial = cpu.get('Serial Number')
if serial == 'Not Specified' or not serial:
serial = cpu.get('ID').replace(' ', '')
self.components.append(
{
"actions": [],
"type": "Processor",
"speed": self.get_cpu_speed(cpu),
"cores": int(cpu.get('Core Count', 1)),
"model": cpu.get('Version'),
"threads": int(cpu.get('Thread Count', 1)),
"manufacturer": cpu.get('Manufacturer'),
"serialNumber": serial,
"brand": cpu.get('Family'),
"address": self.get_cpu_address(cpu),
"bogomips": self.get_bogomips(),
}
)
def get_ram(self):
for ram in self.dmi.get("Memory Device"):
if ram.get('size') == 'No Module Installed':
continue
if not ram.get("Speed"):
continue
self.components.append(
{
"actions": [],
"type": "RamModule",
"size": self.get_ram_size(ram),
"speed": self.get_ram_speed(ram),
"manufacturer": ram.get("Manufacturer", self.default),
"serialNumber": ram.get("Serial Number", self.default),
"interface": ram.get("Type", "DDR"),
"format": ram.get("Form Factor", "DIMM"),
"model": ram.get("Part Number", self.default),
}
)
def get_mother_board(self):
for moder_board in self.dmi.get("Baseboard"):
self.components.append(
{
"actions": [],
"type": "Motherboard",
"version": moder_board.get("Version"),
"serialNumber": moder_board.get("Serial Number", "").strip(),
"manufacturer": moder_board.get("Manufacturer", "").strip(),
"biosDate": self.get_bios_date(),
"ramMaxSize": self.get_max_ram_size(),
"ramSlots": len(self.dmi.get("Memory Device")),
"slots": self.get_ram_slots(),
"model": moder_board.get("Product Name", "").strip(),
"firewire": self.get_firmware_num(),
"pcmcia": self.get_pcmcia_num(),
"serial": self.get_serial_num(),
"usb": self.get_usb_num(),
}
)
def get_graphic(self):
displays = []
get_lshw_child(self.lshw, displays, 'display')
for c in displays:
if not c['configuration'].get('driver', None):
continue
self.components.append(
{
"actions": [],
"type": "GraphicCard",
"memory": "",
"manufacturer": c.get("vendor", self.default),
"model": c.get("product", self.default),
"serialNumber": c.get("serial", self.default),
}
)
def get_data_storage(self):
for sm in self.smart:
if sm.get('smartctl', {}).get('exit_status') == 1:
continue
model = sm.get('model_name')
manufacturer = None
hours = sm.get("power_on_time", {}).get("hours", 0)
if model and len(model.split(" ")) > 1:
mm = model.split(" ")
model = mm[-1]
manufacturer = " ".join(mm[:-1])
self.components.append(
{
"actions": self.sanitize(sm),
"type": self.get_data_storage_type(sm),
"model": model,
"manufacturer": manufacturer,
"serialNumber": sm.get('serial_number'),
"size": self.get_data_storage_size(sm),
"variant": sm.get("firmware_version"),
"interface": self.get_data_storage_interface(sm),
"hours": hours,
}
)
def sanitize(self, action):
return []
def get_bogomips(self):
if not self.hwinfo:
return self.default
bogomips = 0
for row in self.hwinfo:
for cel in row:
if 'BogoMips' in cel:
try:
bogomips += float(cel.split(":")[-1])
except Exception:
pass
return bogomips
def get_networks(self):
networks = []
get_lshw_child(self.lshw, networks, 'network')
for c in networks:
capacity = c.get('capacity')
wireless = bool(c.get('configuration', {}).get('wireless', False))
self.components.append(
{
"actions": [],
"type": "NetworkAdapter",
"model": c.get('product'),
"manufacturer": c.get('vendor'),
"serialNumber": c.get('serial'),
"speed": capacity,
"variant": c.get('version', 1),
"wireless": wireless or False,
"integrated": "PCI:0000:00" in c.get("businfo", ""),
}
)
def get_sound_card(self):
multimedias = []
get_lshw_child(self.lshw, multimedias, 'multimedia')
for c in multimedias:
self.components.append(
{
"actions": [],
"type": "SoundCard",
"model": c.get('product'),
"manufacturer": c.get('vendor'),
"serialNumber": c.get('serial'),
}
)
def get_display(self): # noqa: C901
TECHS = 'CRT', 'TFT', 'LED', 'PDP', 'LCD', 'OLED', 'AMOLED'
for c in self.monitors:
resolution_width, resolution_height = (None,) * 2
refresh, serial, model, manufacturer, size = (None,) * 5
year, week, production_date = (None,) * 3
for x in c:
if "Vendor: " in x:
manufacturer = x.split('Vendor: ')[-1].strip()
if "Model: " in x:
model = x.split('Model: ')[-1].strip()
if "Serial ID: " in x:
serial = x.split('Serial ID: ')[-1].strip()
if " Resolution: " in x:
rs = x.split(' Resolution: ')[-1].strip()
if 'x' in rs:
resolution_width, resolution_height = [
int(r) for r in rs.split('x')
]
if "Frequencies: " in x:
try:
refresh = int(float(x.split(',')[-1].strip()[:-3]))
except Exception:
pass
if 'Year of Manufacture' in x:
year = x.split(': ')[1]
if 'Week of Manufacture' in x:
week = x.split(': ')[1]
if "Size: " in x:
size = self.get_size_monitor(x)
technology = next((t for t in TECHS if t in c[0]), None)
if year and week:
d = '{} {} 0'.format(year, week)
production_date = datetime.strptime(d, '%Y %W %w').isoformat()
self.components.append(
{
"actions": [],
"type": "Display",
"model": model,
"manufacturer": manufacturer,
"serialNumber": serial,
'size': size,
'resolutionWidth': resolution_width,
'resolutionHeight': resolution_height,
"productionDate": production_date,
'technology': technology,
'refreshRate': refresh,
}
)
def get_hwinfo_monitors(self):
for c in self.hwinfo:
monitor = None
external = None
for x in c:
if 'Hardware Class: monitor' in x:
monitor = c
if 'Driver Info' in x:
external = c
if monitor and not external:
self.monitors.append(c)
def get_size_monitor(self, x):
i = 1 / 25.4
t = x.split('Size: ')[-1].strip()
tt = t.split('mm')
if not tt:
return 0
sizes = tt[0].strip()
if 'x' not in sizes:
return 0
w, h = [int(x) for x in sizes.split('x')]
return "{:.2f}".format(np.sqrt(w**2 + h**2) * i)
def get_cpu_address(self, cpu):
default = 64
try:
for ch in self.lshw.get('children', []):
for c in ch.get('children', []):
if c['class'] == 'processor':
return c.get('width', default)
except:
return default
return default
def get_usb_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "USB" in u.get("Port Type", "").upper()
]
)
def get_serial_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "SERIAL" in u.get("Port Type", "").upper()
]
)
def get_firmware_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "FIRMWARE" in u.get("Port Type", "").upper()
]
)
def get_pcmcia_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "PCMCIA" in u.get("Port Type", "").upper()
]
)
def get_bios_date(self):
return self.dmi.get("BIOS")[0].get("Release Date", self.default)
def get_firmware(self):
return self.dmi.get("BIOS")[0].get("Firmware Revision", '1')
def get_max_ram_size(self):
size = 0
for slot in self.dmi.get("Physical Memory Array"):
capacity = slot.get("Maximum Capacity", '0').split(" ")[0]
size += int(capacity)
return size
def get_ram_slots(self):
slots = 0
for x in self.dmi.get("Physical Memory Array"):
slots += int(x.get("Number Of Devices", 0))
return slots
def get_ram_size(self, ram):
memory = ram.get("Size", "0")
return memory
def get_ram_speed(self, ram):
size = ram.get("Speed", "0")
return size
def get_cpu_speed(self, cpu):
speed = cpu.get('Max Speed', "0")
return speed
def get_sku(self):
return self.dmi.get("System")[0].get("SKU Number", self.default).strip()
def get_version(self):
return self.dmi.get("System")[0].get("Version", self.default).strip()
def get_uuid(self):
return self.dmi.get("System")[0].get("UUID", '').strip()
def get_family(self):
return self.dmi.get("System")[0].get("Family", '')
def get_chassis(self):
return self.dmi.get("Chassis")[0].get("Type", '_virtual')
def get_type(self):
chassis_type = self.get_chassis()
return self.translation_to_devicehub(chassis_type)
def translation_to_devicehub(self, original_type):
lower_type = original_type.lower()
CHASSIS_TYPE = {
'Desktop': [
'desktop',
'low-profile',
'tower',
'docking',
'all-in-one',
'pizzabox',
'mini-tower',
'space-saving',
'lunchbox',
'mini',
'stick',
],
'Laptop': [
'portable',
'laptop',
'convertible',
'tablet',
'detachable',
'notebook',
'handheld',
'sub-notebook',
],
'Server': ['server'],
'Computer': ['_virtual'],
}
for k, v in CHASSIS_TYPE.items():
if lower_type in v:
return k
return self.default
def get_chassis_dh(self):
chassis = self.get_chassis()
lower_type = chassis.lower()
for k, v in CHASSIS_DH.items():
if lower_type in v:
return k
return self.default
def get_data_storage_type(self, x):
# TODO @cayop add more SSDS types
SSDS = ["nvme"]
SSD = 'SolidStateDrive'
HDD = 'HardDrive'
type_dev = x.get('device', {}).get('type')
trim = x.get('trim', {}).get("supported") in [True, "true"]
return SSD if type_dev in SSDS or trim else HDD
def get_data_storage_interface(self, x):
interface = x.get('device', {}).get('protocol', 'ATA')
if interface.upper() in DATASTORAGEINTERFACE:
return interface.upper()
txt = "Sid: {}, interface {} is not in DataStorageInterface Enum".format(
self.sid, interface
)
self.errors("{}".format(txt))
def get_data_storage_size(self, x):
return x.get('user_capacity', {}).get('bytes')
def parse_hwinfo(self):
hw_blocks = self.hwinfo_raw.split("\n\n")
return [x.split("\n") for x in hw_blocks]
def loads(self, x):
if isinstance(x, str):
try:
try:
hw = json.loads(x)
except json.decoder.JSONDecodeError:
hw = json.loads(repair_json(x))
return hw
except Exception as ss:
logger.warning("%s", ss)
return {}
return x
def errors(self, txt=None):
if not txt:
return self._errors
logger.error(txt)
self._errors.append("%s", txt)

View file

@ -5,7 +5,7 @@ import logging
from django.core.management.base import BaseCommand
from django.conf import settings
from utils.device import create_property, create_doc, create_index
from utils.device import create_annotation, create_doc, create_index
from user.models import Institution
from evidence.parse import Build
@ -70,7 +70,7 @@ class Command(BaseCommand):
def build_placeholder(self, s, user, f_path):
try:
create_index(s, user)
create_property(s, user, commit=True)
create_annotation(s, user, commit=True)
except Exception as err:
txt = "In placeholder %s \n%s"
logger.warning(txt, f_path, err)

View file

@ -1,107 +0,0 @@
# Generated by Django 5.0.6 on 2024-12-10 19:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("evidence", "0002_alter_annotation_type"),
("user", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="SystemProperty",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("key", models.CharField(max_length=256)),
("value", models.CharField(max_length=256)),
("uuid", models.UUIDField()),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="user.institution",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="UserProperty",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("key", models.CharField(max_length=256)),
("value", models.CharField(max_length=256)),
("uuid", models.UUIDField()),
(
"type",
models.SmallIntegerField(
choices=[(1, "User"), (2, "Document"), (3, "EraseServer")],
default=1,
),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="user.institution",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.DeleteModel(
name="Annotation",
),
migrations.AddConstraint(
model_name="systemproperty",
constraint=models.UniqueConstraint(
fields=("key", "uuid"), name="system_unique_type_key_uuid"
),
),
migrations.AddConstraint(
model_name="userproperty",
constraint=models.UniqueConstraint(
fields=("key", "uuid", "type"), name="user_unique_type_key_uuid"
),
),
]

View file

@ -1,17 +0,0 @@
# Generated by Django 5.0.6 on 2024-12-18 12:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("evidence", "0003_systemproperty_userproperty_delete_annotation_and_more"),
]
operations = [
migrations.RemoveConstraint(
model_name="userproperty",
name="user_unique_type_key_uuid",
),
]

58
evidence/mixin_parse.py Normal file
View file

@ -0,0 +1,58 @@
import logging
from django.conf import settings
from utils.constants import ALGOS
logger = logging.getLogger('django')
class BuildMix:
def __init__(self, evidence_json):
self.json = evidence_json
self.uuid = self.json.get('uuid')
self.manufacturer = ""
self.model = ""
self.serial_number = ""
self.chassis = ""
self.sku = ""
self.mac = ""
self.tpy = ""
self.version = ""
self.get_details()
self.generate_chids()
def get_hid(self, algo):
algorithm = ALGOS.get(algo, [])
hid = ""
for f in algorithm:
if hasattr(self, f):
hid += getattr(self, f)
return hid
def generate_chids(self):
self.algorithms = {
'hidalgo1': self.get_hid('hidalgo1'),
}
if settings.DPP:
self.algorithms["legacy_dpp"] = self.get_hid("legacy_dpp")
def get_doc(self):
self._get_components()
for c in self.components:
c.pop("actions", None)
components = sorted(self.components, key=lambda x: x.get("type"))
device = self.algorithms.get('legacy_dpp')
doc = [("computer", device)]
for c in components:
doc.append((c.get("type"), self.get_id_hw_dpp(c)))
def get_id_hw_dpp(self, d):
algorithm = ALGOS.get("legacy_dpp", [])
hid = ""
for f in algorithm:
hid += d.get(f, '')
return hid

View file

@ -4,46 +4,34 @@ import hashlib
from dmidecode import DMIParse
from django.db import models
from django.db.models import Q
from utils.constants import STR_EXTEND_SIZE, CHASSIS_DH
from evidence.xapian import search
from evidence.parse_details import ParseSnapshot, get_inxi, get_inxi_key
from evidence.parse_details import ParseSnapshot
from evidence.normal_parse_details import get_inxi, get_inxi_key
from user.models import User, Institution
class Property(models.Model):
created = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, blank=True)
key = models.CharField(max_length=STR_EXTEND_SIZE)
value = models.CharField(max_length=STR_EXTEND_SIZE)
class Meta:
#Only for shared behaviour, it is not a table
abstract = True
class SystemProperty(Property):
uuid = models.UUIDField()
class Meta:
constraints = [
models.UniqueConstraint(
fields=["key", "uuid"], name="system_unique_type_key_uuid")
]
class UserProperty(Property):
uuid = models.UUIDField()
class Annotation(models.Model):
class Type(models.IntegerChoices):
SYSTEM = 0, "System"
USER = 1, "User"
DOCUMENT = 2, "Document"
ERASE_SERVER = 3, "EraseServer"
type = models.SmallIntegerField(choices=Type, default=Type.USER)
created = models.DateTimeField(auto_now_add=True)
uuid = models.UUIDField()
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, blank=True)
type = models.SmallIntegerField(choices=Type)
key = models.CharField(max_length=STR_EXTEND_SIZE)
value = models.CharField(max_length=STR_EXTEND_SIZE)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["type", "key", "uuid"], name="unique_type_key_uuid")
]
class Evidence:
@ -54,22 +42,22 @@ class Evidence:
self.created = None
self.dmi = None
self.inxi = None
self.properties = []
self.annotations = []
self.components = []
self.default = "n/a"
self.get_owner()
self.get_time()
def get_properties(self):
self.properties = SystemProperty.objects.filter(
def get_annotations(self):
self.annotations = Annotation.objects.filter(
uuid=self.uuid
).order_by("created")
def get_owner(self):
if not self.properties:
self.get_properties()
a = self.properties.first()
if not self.annotations:
self.get_annotations()
a = self.annotations.first()
if a:
self.owner = a.owner
@ -105,7 +93,7 @@ class Evidence:
self.inxi = ev["output"]
else:
dmidecode_raw = self.doc["data"]["dmidecode"]
inxi_raw = self.doc["data"]["inxi"]
inxi_raw = self.doc.get("data", {}).get("inxi")
self.dmi = DMIParse(dmidecode_raw)
try:
self.inxi = json.loads(inxi_raw)
@ -131,7 +119,7 @@ class Evidence:
self.created = self.doc.get("endTime")
if not self.created:
self.created = self.properties.last().created
self.created = self.annotations.last().created
def get_components(self):
if self.is_legacy():
@ -147,7 +135,7 @@ class Evidence:
return list(self.doc.get('kv').values())[0]
if self.is_legacy():
return self.doc['device']['manufacturer']
return self.doc.get('device', {}).get('manufacturer', '')
if self.inxi:
return self.device_manufacturer
@ -162,7 +150,7 @@ class Evidence:
return list(self.doc.get('kv').values())[1]
if self.is_legacy():
return self.doc['device']['model']
return self.doc.get('device', {}).get('model', '')
if self.inxi:
return self.device_model
@ -171,7 +159,7 @@ class Evidence:
def get_chassis(self):
if self.is_legacy():
return self.doc['device']['model']
return self.doc.get('device', {}).get('model', '')
if self.inxi:
return self.device_chassis
@ -186,7 +174,7 @@ class Evidence:
def get_serial_number(self):
if self.is_legacy():
return self.doc['device']['serialNumber']
return self.doc.get('device', {}).get('serialNumber', '')
if self.inxi:
return self.device_serial_number
@ -201,14 +189,14 @@ class Evidence:
@classmethod
def get_all(cls, user):
return SystemProperty.objects.filter(
return Annotation.objects.filter(
owner=user.institution,
type=Annotation.Type.SYSTEM,
key="hidalgo1",
).order_by("-created").values_list("uuid", "created").distinct()
def set_components(self):
snapshot = ParseSnapshot(self.doc).snapshot_json
self.components = snapshot['components']
self.components = ParseSnapshot(self.doc).components
def is_legacy(self):
if self.doc.get("credentialSubject"):

64
evidence/normal_parse.py Normal file
View file

@ -0,0 +1,64 @@
import json
import logging
from evidence.mixin_parse import BuildMix
from evidence.normal_parse_details import get_inxi_key, get_inxi, ParseSnapshot
logger = logging.getLogger('django')
def get_mac(inxi):
nets = get_inxi_key(inxi, "Network")
networks = [(nets[i], nets[i + 1]) for i in range(0, len(nets) - 1, 2)]
for n, iface in networks:
if get_inxi(n, "port"):
return get_inxi(iface, 'mac')
class Build(BuildMix):
def get_details(self):
self.from_credential()
try:
self.inxi = self.json["data"]["inxi"]
if isinstance(self.inxi, str):
self.inxi = json.loads(self.inxi)
except Exception:
logger.error("No inxi in snapshot %s", self.uuid)
return ""
machine = get_inxi_key(self.inxi, 'Machine')
for m in machine:
system = get_inxi(m, "System")
if system:
self.manufacturer = system
self.model = get_inxi(m, "product")
self.serial_number = get_inxi(m, "serial")
self.chassis = get_inxi(m, "Type")
else:
self.sku = get_inxi(m, "part-nu")
self.mac = get_mac(self.inxi) or ""
if not self.mac:
txt = "Could not retrieve MAC address in snapshot %s"
logger.warning(txt, self.uuid)
def from_credential(self):
if not self.json.get("credentialSubject"):
return
self.uuid = self.json.get("credentialSubject", {}).get("uuid")
self.json.update(self.json["credentialSubject"])
if self.json.get("evidence"):
self.json["data"] = {}
for ev in self.json["evidence"]:
k = ev.get("operation")
if not k:
continue
self.json["data"][k] = ev.get("output")
def _get_components(self):
data = ParseSnapshot(self.json)
self.components = data.components

View file

@ -0,0 +1,402 @@
import json
import logging
from dmidecode import DMIParse
logger = logging.getLogger('django')
def get_inxi_key(inxi, component):
for n in inxi:
for k, v in n.items():
if component in k:
return v
def get_inxi(n, name):
for k, v in n.items():
if f"#{name}" in k:
return v
return ""
class ParseSnapshot:
def __init__(self, snapshot, default="n/a"):
self.default = default
self.dmidecode_raw = snapshot.get("data", {}).get("dmidecode", "{}")
self.smart_raw = snapshot.get("data", {}).get("smartctl", [])
self.inxi_raw = snapshot.get("data", {}).get("inxi", "") or ""
for ev in snapshot.get("evidence", []):
if "dmidecode" == ev.get("operation"):
self.dmidecode_raw = ev["output"]
if "inxi" == ev.get("operation"):
self.inxi_raw = ev["output"]
if "smartctl" == ev.get("operation"):
self.smart_raw = ev["output"]
data = snapshot
if snapshot.get("credentialSubject"):
data = snapshot["credentialSubject"]
self.device = {"actions": []}
self.components = []
self.dmi = DMIParse(self.dmidecode_raw)
self.smart = self.loads(self.smart_raw)
self.inxi = self.loads(self.inxi_raw)
self.set_computer()
self.set_components()
self.snapshot_json = {
"type": "Snapshot",
"device": self.device,
"software": data["software"],
"components": self.components,
"uuid": data['uuid'],
"endTime": data["timestamp"],
"elapsed": 1,
}
def set_computer(self):
machine = get_inxi_key(self.inxi, 'Machine') or []
for m in machine:
system = get_inxi(m, "System")
if system:
self.device['manufacturer'] = system
self.device['model'] = get_inxi(m, "product")
self.device['serialNumber'] = get_inxi(m, "serial")
self.device['type'] = get_inxi(m, "Type")
self.device['chassis'] = self.device['type']
self.device['version'] = get_inxi(m, "v")
else:
self.device['system_uuid'] = get_inxi(m, "uuid")
self.device['sku'] = get_inxi(m, "part-nu")
def set_components(self):
self.get_mother_board()
self.get_cpu()
self.get_ram()
self.get_graphic()
self.get_display()
self.get_networks()
self.get_sound_card()
self.get_data_storage()
self.get_battery()
def get_mother_board(self):
machine = get_inxi_key(self.inxi, 'Machine') or []
mb = {"type": "Motherboard",}
for m in machine:
bios_date = get_inxi(m, "date")
if not bios_date:
continue
mb["manufacturer"] = get_inxi(m, "Mobo")
mb["model"] = get_inxi(m, "model")
mb["serialNumber"] = get_inxi(m, "serial")
mb["version"] = get_inxi(m, "v")
mb["biosDate"] = bios_date
mb["biosVersion"] = self.get_bios_version()
mb["firewire"]: self.get_firmware_num()
mb["pcmcia"]: self.get_pcmcia_num()
mb["serial"]: self.get_serial_num()
mb["usb"]: self.get_usb_num()
self.get_ram_slots(mb)
self.components.append(mb)
def get_ram_slots(self, mb):
memory = get_inxi_key(self.inxi, 'Memory') or []
for m in memory:
slots = get_inxi(m, "slots")
if not slots:
continue
mb["slots"] = slots
mb["ramSlots"] = get_inxi(m, "modules")
mb["ramMaxSize"] = get_inxi(m, "capacity")
def get_cpu(self):
cpu = get_inxi_key(self.inxi, 'CPU') or []
cp = {"type": "Processor"}
vulnerabilities = []
for c in cpu:
base = get_inxi(c, "model")
if base:
cp["model"] = get_inxi(c, "model")
cp["arch"] = get_inxi(c, "arch")
cp["bits"] = get_inxi(c, "bits")
cp["gen"] = get_inxi(c, "gen")
cp["family"] = get_inxi(c, "family")
cp["date"] = get_inxi(c, "built")
continue
des = get_inxi(c, "L1")
if des:
cp["L1"] = des
cp["L2"] = get_inxi(c, "L2")
cp["L3"] = get_inxi(c, "L3")
cp["cpus"] = get_inxi(c, "cpus")
cp["cores"] = get_inxi(c, "cores")
cp["threads"] = get_inxi(c, "threads")
continue
bogo = get_inxi(c, "bogomips")
if bogo:
cp["bogomips"] = bogo
cp["base/boost"] = get_inxi(c, "base/boost")
cp["min/max"] = get_inxi(c, "min/max")
cp["ext-clock"] = get_inxi(c, "ext-clock")
cp["volts"] = get_inxi(c, "volts")
continue
ctype = get_inxi(c, "Type")
if ctype:
v = {"Type": ctype}
status = get_inxi(c, "status")
if status:
v["status"] = status
mitigation = get_inxi(c, "mitigation")
if mitigation:
v["mitigation"] = mitigation
vulnerabilities.append(v)
self.components.append(cp)
def get_ram(self):
memory = get_inxi_key(self.inxi, 'Memory') or []
mem = {"type": "RamModule"}
for m in memory:
base = get_inxi(m, "System RAM")
if base:
mem["size"] = get_inxi(m, "total")
slot = get_inxi(m, "manufacturer")
if slot:
mem["manufacturer"] = slot
mem["model"] = get_inxi(m, "part-no")
mem["serialNumber"] = get_inxi(m, "serial")
mem["speed"] = get_inxi(m, "speed")
mem["bits"] = get_inxi(m, "data")
mem["interface"] = get_inxi(m, "type")
module = get_inxi(m, "modules")
if module:
mem["modules"] = module
self.components.append(mem)
def get_graphic(self):
graphics = get_inxi_key(self.inxi, 'Graphics') or []
for c in graphics:
if not get_inxi(c, "Device") or not get_inxi(c, "vendor"):
continue
self.components.append(
{
"type": "GraphicCard",
"memory": self.get_memory_video(c),
"manufacturer": get_inxi(c, "vendor"),
"model": get_inxi(c, "Device"),
"arch": get_inxi(c, "arch"),
"serialNumber": get_inxi(c, "serial"),
"integrated": True if get_inxi(c, "port") else False
}
)
def get_battery(self):
bats = get_inxi_key(self.inxi, 'Battery') or []
for b in bats:
self.components.append(
{
"type": "Battery",
"model": get_inxi(b, "model"),
"serialNumber": get_inxi(b, "serial"),
"condition": get_inxi(b, "condition"),
"cycles": get_inxi(b, "cycles"),
"volts": get_inxi(b, "volts")
}
)
def get_memory_video(self, c):
memory = get_inxi_key(self.inxi, 'Memory') or []
for m in memory:
igpu = get_inxi(m, "igpu")
agpu = get_inxi(m, "agpu")
ngpu = get_inxi(m, "ngpu")
gpu = get_inxi(m, "gpu")
if igpu or agpu or gpu or ngpu:
return igpu or agpu or gpu or ngpu
return self.default
def get_data_storage(self):
hdds= get_inxi_key(self.inxi, 'Drives') or []
for d in hdds:
usb = get_inxi(d, "type")
if usb == "USB":
continue
serial = get_inxi(d, "serial")
if serial:
hd = {
"type": "Storage",
"manufacturer": get_inxi(d, "vendor"),
"model": get_inxi(d, "model"),
"serialNumber": get_inxi(d, "serial"),
"size": get_inxi(d, "size"),
"speed": get_inxi(d, "speed"),
"interface": get_inxi(d, "tech"),
"firmware": get_inxi(d, "fw-rev")
}
rpm = get_inxi(d, "rpm")
if rpm:
hd["rpm"] = rpm
family = get_inxi(d, "family")
if family:
hd["family"] = family
sata = get_inxi(d, "sata")
if sata:
hd["sata"] = sata
continue
cycles = get_inxi(d, "cycles")
if cycles:
hd['cycles'] = cycles
hd["health"] = get_inxi(d, "health")
hd["time of used"] = get_inxi(d, "on")
hd["read used"] = get_inxi(d, "read-units")
hd["written used"] = get_inxi(d, "written-units")
self.components.append(hd)
continue
hd = {}
def sanitize(self, action):
return []
def get_networks(self):
nets = get_inxi_key(self.inxi, "Network") or []
networks = [(nets[i], nets[i + 1]) for i in range(0, len(nets) - 1, 2)]
for n, iface in networks:
model = get_inxi(n, "Device")
if not model:
continue
interface = ''
for k in n.keys():
if "port" in k:
interface = "Integrated"
if "pcie" in k:
interface = "PciExpress"
if get_inxi(n, "type") == "USB":
interface = "USB"
self.components.append(
{
"type": "NetworkAdapter",
"model": model,
"manufacturer": get_inxi(n, 'vendor'),
"serialNumber": get_inxi(iface, 'mac'),
"speed": get_inxi(n, "speed"),
"interface": interface,
}
)
def get_sound_card(self):
audio = get_inxi_key(self.inxi, "Audio") or []
for c in audio:
model = get_inxi(c, "Device")
if not model:
continue
self.components.append(
{
"type": "SoundCard",
"model": model,
"manufacturer": get_inxi(c, 'vendor'),
"serialNumber": get_inxi(c, 'serial'),
}
)
def get_display(self):
graphics = get_inxi_key(self.inxi, "Graphics") or []
for c in graphics:
if not get_inxi(c, "Monitor"):
continue
self.components.append(
{
"type": "Display",
"model": get_inxi(c, "model"),
"manufacturer": get_inxi(c, "vendor"),
"serialNumber": get_inxi(c, "serial"),
'size': get_inxi(c, "size"),
'diagonal': get_inxi(c, "diag"),
'resolution': get_inxi(c, "res"),
"date": get_inxi(c, "built"),
'ratio': get_inxi(c, "ratio"),
}
)
def get_usb_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "USB" in u.get("Port Type", "").upper()
]
)
def get_serial_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "SERIAL" in u.get("Port Type", "").upper()
]
)
def get_firmware_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "FIRMWARE" in u.get("Port Type", "").upper()
]
)
def get_pcmcia_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "PCMCIA" in u.get("Port Type", "").upper()
]
)
def get_bios_version(self):
return self.dmi.get("BIOS")[0].get("BIOS Revision", '1')
def loads(self, x):
if isinstance(x, str):
try:
return json.loads(x)
except Exception as ss:
logger.warning("%s", ss)
return {}
return x
def errors(self, txt=None):
if not txt:
return self._errors
logger.error(txt)
self._errors.append("%s", txt)

22
evidence/old_parse.py Normal file
View file

@ -0,0 +1,22 @@
import logging
from evidence.mixin_parse import BuildMix
logger = logging.getLogger('django')
class Build(BuildMix):
# This parse is for get info from snapshots created with old workbench
# normaly is worbench 11
def get_details(self):
device = self.json.get('device', {})
self.manufacturer = device.get("manufacturer", '')
self.model = device.get("model", '')
self.chassis = device.get("chassis", '')
self.serial_number = device.get("serialNumber", '')
self.sku = device.get("sku", '')
def _get_components(self):
self.components = self.json.get("components", [])

View file

@ -0,0 +1,13 @@
import logging
logger = logging.getLogger('django')
class ParseSnapshot:
def __init__(self, snapshot, default="n/a"):
self.default = default
self.snapshot_json = snapshot
self.device = snapshot.get("device")
self.components = snapshot.get("components")

View file

@ -2,12 +2,14 @@ import json
import hashlib
import logging
from dmidecode import DMIParse
from evidence import legacy_parse
from evidence import old_parse
from evidence import normal_parse
from evidence.parse_details import ParseSnapshot
from evidence.models import SystemProperty
from evidence.models import Annotation
from evidence.xapian import index
from evidence.parse_details import get_inxi_key, get_inxi
from evidence.normal_parse_details import get_inxi_key, get_inxi
from django.conf import settings
if settings.DPP:
@ -24,9 +26,83 @@ def get_mac(inxi):
if get_inxi(n, "port"):
return get_inxi(iface, 'mac')
class Build:
def __init__(self, evidence_json, user, check=False):
"""
This Build do the save in xapian as document, in Annotations and do
register in dlt if is configured for that.
We have 4 cases for parser diferents snapshots than come from workbench.
1) worbench 11 is old_parse.
2) legacy is the worbench-script when create a snapshot for devicehub-teal
3) some snapshots come as a credential. In this case is parsed as normal_parse
4) normal snapshot from worbench-script is the most basic and is parsed as normal_parse
"""
self.evidence = evidence_json.copy()
self.uuid = self.evidence.get('uuid')
self.user = user
if evidence_json.get("credentialSubject"):
self.build = normal_parse.Build(evidence_json)
self.uuid = evidence_json.get("credentialSubject", {}).get("uuid")
elif evidence_json.get("software") != "workbench-script":
self.build = old_parse.Build(evidence_json)
elif evidence_json.get("data",{}).get("lshw"):
self.build = legacy_parse.Build(evidence_json)
else:
self.build = normal_parse.Build(evidence_json)
if check:
return
self.index()
self.create_annotations()
if settings.DPP:
self.register_device_dlt()
def index(self):
snap = json.dumps(self.evidence)
index(self.user.institution, self.uuid, snap)
def create_annotations(self):
annotation = Annotation.objects.filter(
uuid=self.uuid,
owner=self.user.institution,
type=Annotation.Type.SYSTEM,
)
if annotation:
txt = "Warning: Snapshot %s already registered (annotation exists)"
logger.warning(txt, self.uuid)
return
for k, v in self.build.algorithms.items():
Annotation.objects.create(
uuid=self.uuid,
owner=self.user.institution,
user=self.user,
type=Annotation.Type.SYSTEM,
key=k,
value=self.sign(v)
)
def sign(self, doc):
return hashlib.sha3_256(doc.encode()).hexdigest()
def register_device_dlt(self):
legacy_dpp = self.build.algorithms.get('legacy_dpp')
chid = self.sign(legacy_dpp)
phid = self.sign(json.dumps(self.build.get_doc()))
register_device_dlt(chid, phid, self.uuid, self.user)
register_passport_dlt(chid, phid, self.uuid, self.user)
class Build2:
def __init__(self, evidence_json, user, check=False):
if evidence_json.get("data",{}).get("lshw"):
if evidence_json.get("software") == "workbench-script":
return legacy_parse.Build(evidence_json, user, check=check)
self.evidence = evidence_json.copy()
self.json = evidence_json.copy()
@ -51,7 +127,7 @@ class Build:
return
self.index()
self.create_properties()
self.create_annotations()
if settings.DPP:
self.register_device_dlt()
@ -123,22 +199,24 @@ class Build:
return doc
def create_properties(self):
property = SystemProperty.objects.filter(
def create_annotations(self):
annotation = Annotation.objects.filter(
uuid=self.uuid,
owner=self.user.institution,
type=Annotation.Type.SYSTEM,
)
if property:
txt = "Warning: Snapshot %s already registered (property exists)"
if annotation:
txt = "Warning: Snapshot %s already registered (annotation exists)"
logger.warning(txt, self.uuid)
return
for k, v in self.algorithms.items():
SystemProperty.objects.create(
Annotation.objects.create(
uuid=self.uuid,
owner=self.user.institution,
user=self.user,
type=Annotation.Type.SYSTEM,
key=k,
value=v
)

View file

@ -1,407 +1,38 @@
import re
import json
import logging
import numpy as np
from datetime import datetime
from dmidecode import DMIParse
from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE
from evidence import (
legacy_parse_details,
normal_parse_details,
old_parse_details
)
logger = logging.getLogger('django')
def get_inxi_key(inxi, component):
for n in inxi:
for k, v in n.items():
if component in k:
return v
def get_inxi(n, name):
for k, v in n.items():
if f"#{name}" in k:
return v
return ""
class ParseSnapshot:
def __init__(self, snapshot, default="n/a"):
self.default = default
self.dmidecode_raw = snapshot.get("data", {}).get("dmidecode", "{}")
self.smart_raw = snapshot.get("data", {}).get("smartctl", [])
self.inxi_raw = snapshot.get("data", {}).get("inxi", "") or ""
for ev in snapshot.get("evidence", []):
if "dmidecode" == ev.get("operation"):
self.dmidecode_raw = ev["output"]
if "inxi" == ev.get("operation"):
self.inxi_raw = ev["output"]
if "smartctl" == ev.get("operation"):
self.smart_raw = ev["output"]
data = snapshot
if snapshot.get("credentialSubject"):
data = snapshot["credentialSubject"]
if snapshot.get("credentialSubject"):
self.build = normal_parse_details.ParseSnapshot(
snapshot,
default=default
)
elif snapshot.get("software") != "workbench-script":
self.build = old_parse_details.ParseSnapshot(
snapshot,
default=default
)
elif snapshot.get("data",{}).get("lshw"):
self.build = legacy_parse_details.ParseSnapshot(
snapshot,
default=default
)
else:
self.build = normal_parse_details.ParseSnapshot(
snapshot,
default=default
)
self.device = {"actions": []}
self.components = []
self.dmi = DMIParse(self.dmidecode_raw)
self.smart = self.loads(self.smart_raw)
self.inxi = self.loads(self.inxi_raw)
self.set_computer()
self.set_components()
self.snapshot_json = {
"type": "Snapshot",
"device": self.device,
"software": data["software"],
"components": self.components,
"uuid": data['uuid'],
"endTime": data["timestamp"],
"elapsed": 1,
}
def set_computer(self):
machine = get_inxi_key(self.inxi, 'Machine') or []
for m in machine:
system = get_inxi(m, "System")
if system:
self.device['manufacturer'] = system
self.device['model'] = get_inxi(m, "product")
self.device['serialNumber'] = get_inxi(m, "serial")
self.device['type'] = get_inxi(m, "Type")
self.device['chassis'] = self.device['type']
self.device['version'] = get_inxi(m, "v")
else:
self.device['system_uuid'] = get_inxi(m, "uuid")
self.device['sku'] = get_inxi(m, "part-nu")
def set_components(self):
self.get_mother_board()
self.get_cpu()
self.get_ram()
self.get_graphic()
self.get_display()
self.get_networks()
self.get_sound_card()
self.get_data_storage()
self.get_battery()
def get_mother_board(self):
machine = get_inxi_key(self.inxi, 'Machine') or []
mb = {"type": "Motherboard",}
for m in machine:
bios_date = get_inxi(m, "date")
if not bios_date:
continue
mb["manufacturer"] = get_inxi(m, "Mobo")
mb["model"] = get_inxi(m, "model")
mb["serialNumber"] = get_inxi(m, "serial")
mb["version"] = get_inxi(m, "v")
mb["biosDate"] = bios_date
mb["biosVersion"] = self.get_bios_version()
mb["firewire"]: self.get_firmware_num()
mb["pcmcia"]: self.get_pcmcia_num()
mb["serial"]: self.get_serial_num()
mb["usb"]: self.get_usb_num()
self.get_ram_slots(mb)
self.components.append(mb)
def get_ram_slots(self, mb):
memory = get_inxi_key(self.inxi, 'Memory') or []
for m in memory:
slots = get_inxi(m, "slots")
if not slots:
continue
mb["slots"] = slots
mb["ramSlots"] = get_inxi(m, "modules")
mb["ramMaxSize"] = get_inxi(m, "capacity")
def get_cpu(self):
cpu = get_inxi_key(self.inxi, 'CPU') or []
cp = {"type": "Processor"}
vulnerabilities = []
for c in cpu:
base = get_inxi(c, "model")
if base:
cp["model"] = get_inxi(c, "model")
cp["arch"] = get_inxi(c, "arch")
cp["bits"] = get_inxi(c, "bits")
cp["gen"] = get_inxi(c, "gen")
cp["family"] = get_inxi(c, "family")
cp["date"] = get_inxi(c, "built")
continue
des = get_inxi(c, "L1")
if des:
cp["L1"] = des
cp["L2"] = get_inxi(c, "L2")
cp["L3"] = get_inxi(c, "L3")
cp["cpus"] = get_inxi(c, "cpus")
cp["cores"] = get_inxi(c, "cores")
cp["threads"] = get_inxi(c, "threads")
continue
bogo = get_inxi(c, "bogomips")
if bogo:
cp["bogomips"] = bogo
cp["base/boost"] = get_inxi(c, "base/boost")
cp["min/max"] = get_inxi(c, "min/max")
cp["ext-clock"] = get_inxi(c, "ext-clock")
cp["volts"] = get_inxi(c, "volts")
continue
ctype = get_inxi(c, "Type")
if ctype:
v = {"Type": ctype}
status = get_inxi(c, "status")
if status:
v["status"] = status
mitigation = get_inxi(c, "mitigation")
if mitigation:
v["mitigation"] = mitigation
vulnerabilities.append(v)
self.components.append(cp)
def get_ram(self):
memory = get_inxi_key(self.inxi, 'Memory') or []
mem = {"type": "RamModule"}
for m in memory:
base = get_inxi(m, "System RAM")
if base:
mem["size"] = get_inxi(m, "total")
slot = get_inxi(m, "manufacturer")
if slot:
mem["manufacturer"] = slot
mem["model"] = get_inxi(m, "part-no")
mem["serialNumber"] = get_inxi(m, "serial")
mem["speed"] = get_inxi(m, "speed")
mem["bits"] = get_inxi(m, "data")
mem["interface"] = get_inxi(m, "type")
module = get_inxi(m, "modules")
if module:
mem["modules"] = module
self.components.append(mem)
def get_graphic(self):
graphics = get_inxi_key(self.inxi, 'Graphics') or []
for c in graphics:
if not get_inxi(c, "Device") or not get_inxi(c, "vendor"):
continue
self.components.append(
{
"type": "GraphicCard",
"memory": self.get_memory_video(c),
"manufacturer": get_inxi(c, "vendor"),
"model": get_inxi(c, "Device"),
"arch": get_inxi(c, "arch"),
"serialNumber": get_inxi(c, "serial"),
"integrated": True if get_inxi(c, "port") else False
}
)
def get_battery(self):
bats = get_inxi_key(self.inxi, 'Battery') or []
for b in bats:
self.components.append(
{
"type": "Battery",
"model": get_inxi(b, "model"),
"serialNumber": get_inxi(b, "serial"),
"condition": get_inxi(b, "condition"),
"cycles": get_inxi(b, "cycles"),
"volts": get_inxi(b, "volts")
}
)
def get_memory_video(self, c):
memory = get_inxi_key(self.inxi, 'Memory') or []
for m in memory:
igpu = get_inxi(m, "igpu")
agpu = get_inxi(m, "agpu")
ngpu = get_inxi(m, "ngpu")
gpu = get_inxi(m, "gpu")
if igpu or agpu or gpu or ngpu:
return igpu or agpu or gpu or ngpu
return self.default
def get_data_storage(self):
hdds= get_inxi_key(self.inxi, 'Drives') or []
for d in hdds:
usb = get_inxi(d, "type")
if usb == "USB":
continue
serial = get_inxi(d, "serial")
if serial:
hd = {
"type": "Storage",
"manufacturer": get_inxi(d, "vendor"),
"model": get_inxi(d, "model"),
"serialNumber": get_inxi(d, "serial"),
"size": get_inxi(d, "size"),
"speed": get_inxi(d, "speed"),
"interface": get_inxi(d, "tech"),
"firmware": get_inxi(d, "fw-rev")
}
rpm = get_inxi(d, "rpm")
if rpm:
hd["rpm"] = rpm
family = get_inxi(d, "family")
if family:
hd["family"] = family
sata = get_inxi(d, "sata")
if sata:
hd["sata"] = sata
continue
cycles = get_inxi(d, "cycles")
if cycles:
hd['cycles'] = cycles
hd["health"] = get_inxi(d, "health")
hd["time of used"] = get_inxi(d, "on")
hd["read used"] = get_inxi(d, "read-units")
hd["written used"] = get_inxi(d, "written-units")
self.components.append(hd)
continue
hd = {}
def sanitize(self, action):
return []
def get_networks(self):
nets = get_inxi_key(self.inxi, "Network") or []
networks = [(nets[i], nets[i + 1]) for i in range(0, len(nets) - 1, 2)]
for n, iface in networks:
model = get_inxi(n, "Device")
if not model:
continue
interface = ''
for k in n.keys():
if "port" in k:
interface = "Integrated"
if "pcie" in k:
interface = "PciExpress"
if get_inxi(n, "type") == "USB":
interface = "USB"
self.components.append(
{
"type": "NetworkAdapter",
"model": model,
"manufacturer": get_inxi(n, 'vendor'),
"serialNumber": get_inxi(iface, 'mac'),
"speed": get_inxi(n, "speed"),
"interface": interface,
}
)
def get_sound_card(self):
audio = get_inxi_key(self.inxi, "Audio") or []
for c in audio:
model = get_inxi(c, "Device")
if not model:
continue
self.components.append(
{
"type": "SoundCard",
"model": model,
"manufacturer": get_inxi(c, 'vendor'),
"serialNumber": get_inxi(c, 'serial'),
}
)
def get_display(self):
graphics = get_inxi_key(self.inxi, "Graphics") or []
for c in graphics:
if not get_inxi(c, "Monitor"):
continue
self.components.append(
{
"type": "Display",
"model": get_inxi(c, "model"),
"manufacturer": get_inxi(c, "vendor"),
"serialNumber": get_inxi(c, "serial"),
'size': get_inxi(c, "size"),
'diagonal': get_inxi(c, "diag"),
'resolution': get_inxi(c, "res"),
"date": get_inxi(c, "built"),
'ratio': get_inxi(c, "ratio"),
}
)
def get_usb_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "USB" in u.get("Port Type", "").upper()
]
)
def get_serial_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "SERIAL" in u.get("Port Type", "").upper()
]
)
def get_firmware_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "FIRMWARE" in u.get("Port Type", "").upper()
]
)
def get_pcmcia_num(self):
return len(
[
u
for u in self.dmi.get("Port Connector")
if "PCMCIA" in u.get("Port Type", "").upper()
]
)
def get_bios_version(self):
return self.dmi.get("BIOS")[0].get("BIOS Revision", '1')
def loads(self, x):
if isinstance(x, str):
try:
return json.loads(x)
except Exception as ss:
logger.warning("%s", ss)
return {}
return x
def errors(self, txt=None):
if not txt:
return self._errors
logger.error(txt)
self._errors.append("%s", txt)
self.default = default
self.device = self.build.snapshot_json.get("device")
self.components = self.build.snapshot_json.get("components")

View file

@ -45,8 +45,9 @@
</th>
</tr>
</thead>
{% for snap in object.properties %}
{% for snap in object.annotations %}
<tbody>
{% if snap.type == 0 %}
<tr>
<td>
{{ snap.key }}
@ -62,6 +63,7 @@
</small>
</td>
</tr>
{% endif %}
</tbody>
{% endfor %}
</table>
@ -92,7 +94,7 @@
</div>
{% if form.tag.value %}
<div class="col-1">
<a class="btn btn-yellow" href="{% url 'evidence:delete_tag' form.pk %}">{% translate "Delete" %}</a>
<a class="btn btn-yellow" href="{% url 'evidence:delete_annotation' form.pk %}">{% translate "Delete" %}</a>
</div>
{% endif %}
</div>

View file

@ -20,5 +20,5 @@ urlpatterns = [
path("<uuid:pk>", views.EvidenceView.as_view(), name="details"),
path("<uuid:pk>/eraseserver", views.EraseServerView.as_view(), name="erase_server"),
path("<uuid:pk>/download", views.DownloadEvidenceView.as_view(), name="download"),
path("tag/<str:pk>/delete", views.DeleteEvidenceTagView.as_view(), name="delete_tag"),
path('annotation/<int:pk>/del', views.AnnotationDeleteView.as_view(), name='delete_annotation'),
]

View file

@ -12,9 +12,8 @@ from django.views.generic.edit import (
FormView,
)
from action.models import DeviceLog
from dashboard.mixins import DashboardView, Http403
from evidence.models import SystemProperty, UserProperty, Evidence
from evidence.models import Evidence, Annotation
from evidence.forms import (
UploadForm,
UserTagForm,
@ -96,7 +95,7 @@ class EvidenceView(DashboardView, FormView):
if self.object.owner != self.request.user.institution:
raise Http403
self.object.get_properties()
self.object.get_annotations()
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
@ -142,6 +141,33 @@ class DownloadEvidenceView(DashboardView, TemplateView):
return response
class AnnotationDeleteView(DashboardView, DeleteView):
model = Annotation
def get(self, request, *args, **kwargs):
self.pk = kwargs['pk']
try:
referer = self.request.META["HTTP_REFERER"]
path_referer = urlparse(referer).path
resolver_match = resolve(path_referer)
url_name = resolver_match.view_name
kwargs_view = resolver_match.kwargs
except:
# if is not possible resolve the reference path return 404
raise Http404
self.object = get_object_or_404(
self.model,
pk=self.pk,
owner=self.request.user.institution
)
self.object.delete()
return redirect(url_name, **kwargs_view)
class EraseServerView(DashboardView, FormView):
template_name = "ev_eraseserver.html"
section = "evidences"
@ -156,7 +182,7 @@ class EraseServerView(DashboardView, FormView):
if self.object.owner != self.request.user.institution:
raise Http403
self.object.get_properties()
self.object.get_annotations()
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
@ -185,35 +211,3 @@ class EraseServerView(DashboardView, FormView):
def get_success_url(self):
success_url = reverse_lazy('evidence:details', args=[self.pk])
return success_url
class DeleteEvidenceTagView(DashboardView, DeleteView):
model = SystemProperty
def get_queryset(self):
# only those with 'CUSTOM_ID'
return SystemProperty.objects.filter(owner=self.request.user.institution, key='CUSTOM_ID')
def get(self, request, *args, **kwargs):
self.object = self.get_object()
message = _("<Deleted> Evidence Tag: {}").format(self.object.value)
DeviceLog.objects.create(
snapshot_uuid=self.object.uuid,
event=message,
user=self.request.user,
institution=self.request.user.institution
)
self.object.delete()
messages.info(self.request, _("Evicende Tag deleted successfully."))
return self.handle_success()
def handle_success(self):
return redirect(self.get_success_url())
def get_success_url(self):
return self.request.META.get(
'HTTP_REFERER',
reverse_lazy('evidence:details', args=[self.object.uuid])
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,77 +0,0 @@
# Generated by Django 5.0.6 on 2024-12-10 19:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("lot", "0002_alter_lot_closed"),
("user", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="LotProperty",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("key", models.CharField(max_length=256)),
("value", models.CharField(max_length=256)),
(
"type",
models.SmallIntegerField(
choices=[
(0, "System"),
(1, "User"),
(2, "Document"),
(3, "EraseServer"),
],
default=1,
),
),
(
"lot",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="lot.lot"
),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="user.institution",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.DeleteModel(
name="LotAnnotation",
),
migrations.AddConstraint(
model_name="lotproperty",
constraint=models.UniqueConstraint(
fields=("key", "lot", "type"), name="lot_unique_type_key_lot"
),
),
]

View file

@ -1,24 +0,0 @@
# Generated by Django 5.0.6 on 2024-12-18 12:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("lot", "0003_lotproperty_delete_lotannotation_and_more"),
]
operations = [
migrations.RemoveConstraint(
model_name="lotproperty",
name="lot_unique_type_key_lot",
),
migrations.AlterField(
model_name="lotproperty",
name="type",
field=models.SmallIntegerField(
choices=[(0, "System"), (1, "User"), (2, "Document")], default=1
),
),
]

View file

@ -6,9 +6,9 @@ from utils.constants import (
STR_EXTEND_SIZE,
)
from user.models import User, Institution
from evidence.models import Property
from user.models import User, Institution
# from device.models import Device
# from evidence.models import Annotation
class LotTag(models.Model):
@ -45,12 +45,17 @@ class Lot(models.Model):
for d in DeviceLot.objects.filter(lot=self, device_id=v):
d.delete()
class LotProperty (Property):
lot = models.ForeignKey(Lot, on_delete=models.CASCADE)
class LotAnnotation(models.Model):
class Type(models.IntegerChoices):
SYSTEM = 0, "System"
SYSTEM= 0, "System"
USER = 1, "User"
DOCUMENT = 2, "Document"
type = models.SmallIntegerField(choices=Type.choices, default=Type.USER)
created = models.DateTimeField(auto_now_add=True)
lot = models.ForeignKey(Lot, on_delete=models.CASCADE)
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
type = models.SmallIntegerField(choices=Type)
key = models.CharField(max_length=STR_EXTEND_SIZE)
value = models.CharField(max_length=STR_EXTEND_SIZE)

View file

@ -0,0 +1,48 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col">
<h3>Lot {{ lot.name }}</h3>
</div>
</div>
<div class="row">
<div class="tab-pane fade show active" id="details">
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
<a href="{% url 'lot:add_annotation' lot.pk %}" class="btn btn-primary">
<i class="bi bi-plus"></i>
Add new annotation
<span class="caret"></span>
</a>
</div>
<h5 class="card-title mt-2">Annotations</h5>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Key</th>
<th scope="col">Value</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm">Created on</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in annotations %}
<tr>
<td>{{ a.key }}</td>
<td>{{ a.value }}</td>
<td>{{ a.created }}</td>
<td></td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View file

@ -1,105 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col">
<h3>Lot {{ lot.name }}</h3>
</div>
</div>
<div class="row">
<div class="tab-pane fade show active" id="details">
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
<a href="{% url 'lot:add_property' lot.pk %}" class="btn btn-primary">
<i class="bi bi-plus"></i>
Add new lot Property
<span class="caret"></span>
</a>
</div>
<h5 class="card-title mt-2">Properties</h5>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Key</th>
<th scope="col">Value</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm">Created on</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in properties %}
<tr>
<td>{{ a.key }}</td>
<td>{{ a.value }}</td>
<td>{{ a.created }}</td>
<td class="text-center">
<a href="#" class="text-info" data-bs-toggle="modal" data-bs-target="#editPropertyModal{{ a.id }}">
<i class="bi bi-pencil"></i>
</a>
</td>
<td class="text-center">
<a href="#" class="text-danger" data-bs-toggle="modal" data-bs-target="#deletePropertyModal{{ a.id }}">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
<div class="modal fade" id="editPropertyModal{{ a.id }}" tabindex="-1" aria-labelledby="editPropertyModalLabel{{ a.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editPropertyModalLabel{{ a.id }}">{% trans "Edit Property" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'lot:update_property' a.id %}">
{% csrf_token %}
<div class="mb-3">
<label for="propertyKey{{ a.id }}" class="form-label">{% trans "Key" %}</label>
<input type="text" class="form-control" id="propertyKey{{ a.id }}" name="key" value="{{ a.key }}" required>
</div>
<div class="mb-3">
<label for="propertyValue{{ a.id }}" class="form-label">{% trans "Value" %}</label>
<input type="text" class="form-control" id="propertyValue{{ a.id }}" name="value" value="{{ a.value }}" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Save changes" %}</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="deletePropertyModal{{ a.id }}" tabindex="-1" aria-labelledby="deletePropertyModalLabel{{ a.id }}" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deletePropertyModalLabel{{ a.id }}">{% trans "Delete Property" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans 'Close' %}"></button>
</div>
<div class="modal-body">
<p>{% trans "Are you sure you want to delete this property?" %}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<form method="post" action="{% url 'lot:delete_property' a.id %}">
{% csrf_token %}
<button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View file

@ -12,8 +12,6 @@ urlpatterns = [
path("tag/<int:pk>/", views.LotsTagsView.as_view(), name="tag"),
path("<int:pk>/document/", views.LotDocumentsView.as_view(), name="documents"),
path("<int:pk>/document/add", views.LotAddDocumentView.as_view(), name="add_document"),
path("<int:pk>/property", views.LotPropertiesView.as_view(), name="properties"),
path("<int:pk>/property/add", views.AddLotPropertyView.as_view(), name="add_property"),
path("<int:pk>/property/update", views.UpdateLotPropertyView.as_view(), name="update_property"),
path("<int:pk>/property/delete", views.DeleteLotPropertyView.as_view(), name="delete_property"),
path("<int:pk>/annotation", views.LotAnnotationsView.as_view(), name="annotations"),
path("<int:pk>/annotation/add", views.LotAddAnnotationView.as_view(), name="add_annotation"),
]

View file

@ -1,6 +1,5 @@
from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404, redirect, Http404
from django.contrib import messages
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
@ -10,9 +9,10 @@ from django.views.generic.edit import (
FormView,
)
from dashboard.mixins import DashboardView
from lot.models import Lot, LotTag, LotProperty
from lot.models import Lot, LotTag, LotAnnotation
from lot.forms import LotsForm
class NewLotView(DashboardView, CreateView):
template_name = "new_lot.html"
title = _("New lot")
@ -143,18 +143,18 @@ class LotsTagsView(DashboardView, TemplateView):
class LotAddDocumentView(DashboardView, CreateView):
template_name = "new_property.html"
template_name = "new_annotation.html"
title = _("New Document")
breadcrumb = "Device / New document"
success_url = reverse_lazy('dashboard:unassigned_devices')
model = LotProperty
model = LotAnnotation
fields = ("key", "value")
def form_valid(self, form):
form.instance.owner = self.request.user.institution
form.instance.user = self.request.user
form.instance.lot = self.lot
form.instance.type = LotProperty.Type.DOCUMENT
form.instance.type = LotAnnotation.Type.DOCUMENT
response = super().form_valid(form)
return response
@ -169,16 +169,16 @@ class LotAddDocumentView(DashboardView, CreateView):
class LotDocumentsView(DashboardView, TemplateView):
template_name = "documents.html"
title = _("New Document")
breadcrumb = "Devicce / New document"
breadcrumb = "Device / New document"
def get_context_data(self, **kwargs):
self.pk = kwargs.get('pk')
context = super().get_context_data(**kwargs)
lot = get_object_or_404(Lot, owner=self.request.user.institution, id=self.pk)
documents = LotProperty.objects.filter(
documents = LotAnnotation.objects.filter(
lot=lot,
owner=self.request.user.institution,
type=LotProperty.Type.DOCUMENT,
type=LotAnnotation.Type.DOCUMENT,
)
context.update({
'lot': lot,
@ -189,106 +189,48 @@ class LotDocumentsView(DashboardView, TemplateView):
return context
class LotPropertiesView(DashboardView, TemplateView):
template_name = "properties.html"
title = _("New Lot Property")
breadcrumb = "Lot / New property"
class LotAnnotationsView(DashboardView, TemplateView):
template_name = "annotations.html"
title = _("New Annotation")
breadcrumb = "Device / New annotation"
def get_context_data(self, **kwargs):
self.pk = kwargs.get('pk')
context = super().get_context_data(**kwargs)
lot = get_object_or_404(Lot, owner=self.request.user.institution, id=self.pk)
properties = LotProperty.objects.filter(
annotations = LotAnnotation.objects.filter(
lot=lot,
owner=self.request.user.institution,
type=LotProperty.Type.USER,
type=LotAnnotation.Type.USER,
)
context.update({
'lot': lot,
'properties': properties,
'annotations': annotations,
'title': self.title,
'breadcrumb': self.breadcrumb
})
return context
class AddLotPropertyView(DashboardView, CreateView):
template_name = "new_property.html"
title = _("New Lot Property")
breadcrumb = "Device / New property"
class LotAddAnnotationView(DashboardView, CreateView):
template_name = "new_annotation.html"
title = _("New Annotation")
breadcrumb = "Device / New annotation"
success_url = reverse_lazy('dashboard:unassigned_devices')
model = LotProperty
model = LotAnnotation
fields = ("key", "value")
def form_valid(self, form):
form.instance.owner = self.request.user.institution
form.instance.user = self.request.user
form.instance.lot = self.lot
form.instance.type = LotProperty.Type.USER
form.instance.type = LotAnnotation.Type.USER
response = super().form_valid(form)
return response
def get_form_kwargs(self):
pk = self.kwargs.get('pk')
self.lot = get_object_or_404(Lot, pk=pk, owner=self.request.user.institution)
self.success_url = reverse_lazy('lot:properties', args=[pk])
self.success_url = reverse_lazy('lot:annotations', args=[pk])
kwargs = super().get_form_kwargs()
return kwargs
class UpdateLotPropertyView(DashboardView, UpdateView):
template_name = "properties.html"
title = _("Update lot Property")
breadcrumb = "Lot / Update Property"
model = LotProperty
fields = ("key", "value")
def get_form_kwargs(self):
pk = self.kwargs.get('pk')
lot_property = get_object_or_404(LotProperty, pk=pk, owner=self.request.user.institution)
if not lot_property:
raise Http404
kwargs = super().get_form_kwargs()
kwargs['instance'] = lot_property
return kwargs
def form_valid(self, form):
old_key= self.object.key
old_value = self.object.value
new_key = form.cleaned_data['key']
new_value = form.cleaned_data['value']
form.instance.owner = self.request.user.institution
form.instance.user = self.request.user
form.instance.type = LotProperty.Type.USER
response = super().form_valid(form)
messages.success(self.request, _("Lot property updated successfully."))
return response
def get_success_url(self):
return self.request.META.get('HTTP_REFERER', reverse_lazy('device:details', args=[self.object.pk]))
class DeleteLotPropertyView(DashboardView, DeleteView):
model = LotProperty
def post(self, request, *args, **kwargs):
self.pk = kwargs['pk']
referer = request.META.get('HTTP_REFERER')
if not referer:
raise Http404("No referer header found")
self.object = get_object_or_404(
self.model,
pk=self.pk,
owner=self.request.user.institution
)
old_value = self.object.key
self.object.delete()
messages.success(self.request, _("Lot property deleted successfully."))
# Redirect back to the original URL
return redirect(referer)

View file

@ -13,7 +13,7 @@ HID_ALGO1 = [
"manufacturer",
"model",
"chassis",
"serialNumber",
"serial_number",
"sku"
]
@ -21,7 +21,7 @@ LEGACY_DPP = [
"manufacturer",
"model",
"chassis",
"serialNumber",
"serial_number",
"sku",
"type",
"version"

View file

@ -6,7 +6,7 @@ import logging
from django.core.exceptions import ValidationError
from evidence.xapian import index
from evidence.models import SystemProperty
from evidence.models import Annotation
from device.models import Device
@ -68,7 +68,7 @@ def create_doc(data):
return doc
def create_property(doc, user, commit=False):
def create_annotation(doc, user, commit=False):
if not doc or not doc.get('uuid') or not doc.get("CUSTOMER_ID"):
return []
@ -76,23 +76,25 @@ def create_property(doc, user, commit=False):
'uuid': doc['uuid'],
'owner': user.institution,
'user': user,
'type': Annotation.Type.SYSTEM,
'key': 'CUSTOMER_ID',
'value': doc['CUSTOMER_ID'],
}
if commit:
prop = SystemProperty.objects.filter(
annotation = Annotation.objects.filter(
uuid=doc["uuid"],
owner=user.institution,
type=Annotation.Type.SYSTEM,
)
if prop:
txt = "Warning: Snapshot %s already registered (system property exists)"
if annotation:
txt = "Warning: Snapshot %s already registered (annotation exists)"
logger.warning(txt, doc["uuid"])
return prop
return annotation
return SystemProperty.objects.create(**data)
return Annotation.objects.create(**data)
return SystemProperty(**data)
return Annotation(**data)
def create_index(doc, user):