Compare commits

..

No commits in common. "main" and "api_bugfix_public-url" have entirely different histories.

37 changed files with 360 additions and 1378 deletions

View File

@ -1,12 +1,11 @@
DOMAIN=localhost DOMAIN=localhost
# note that with DEBUG=true, logs are more verbose (include tracebacks) DEMO=false
DEBUG=true
DEMO=true
STATIC_ROOT=/tmp/static/ STATIC_ROOT=/tmp/static/
MEDIA_ROOT=/tmp/media/ MEDIA_ROOT=/tmp/media/
ALLOWED_HOSTS=localhost,localhost:8000,127.0.0.1, ALLOWED_HOSTS=localhost,localhost:8000,127.0.0.1,
DOMAIN=localhost DOMAIN=localhost
DEBUG=True
EMAIL_HOST="mail.example.org" EMAIL_HOST="mail.example.org"
EMAIL_HOST_USER="fillme_noreply" EMAIL_HOST_USER="fillme_noreply"
EMAIL_HOST_PASSWORD="fillme_passwd" EMAIL_HOST_PASSWORD="fillme_passwd"

View File

@ -6,11 +6,9 @@ from django.urls import path
app_name = 'api' app_name = 'api'
urlpatterns = [ urlpatterns = [
path('v1/snapshot/', views.NewSnapshotView.as_view(), name='new_snapshot'), path('snapshot/', views.NewSnapshot, name='new_snapshot'),
path('v1/annotation/<str:pk>/', views.AddAnnotationView.as_view(), name='new_annotation'), path('tokens/', views.TokenView.as_view(), name='tokens'),
path('v1/device/<str:pk>/', views.DetailsDeviceView.as_view(), name='device'), path('tokens/new', views.TokenNewView.as_view(), name='new_token'),
path('v1/tokens/', views.TokenView.as_view(), name='tokens'), path("tokens/<int:pk>/edit", views.EditTokenView.as_view(), name="edit_token"),
path('v1/tokens/new', views.TokenNewView.as_view(), name='new_token'), path('tokens/<int:pk>/del', views.TokenDeleteView.as_view(), name='delete_token'),
path("v1/tokens/<int:pk>/edit", views.EditTokenView.as_view(), name="edit_token"),
path('v1/tokens/<int:pk>/del', views.TokenDeleteView.as_view(), name='delete_token'),
] ]

View File

@ -1,16 +1,12 @@
import json import json
import uuid
import logging
from uuid import uuid4 from uuid import uuid4
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.conf import settings
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from django.views.generic.edit import ( from django.views.generic.edit import (
CreateView, CreateView,
@ -19,124 +15,81 @@ from django.views.generic.edit import (
) )
from utils.save_snapshots import move_json, save_in_disk from utils.save_snapshots import move_json, save_in_disk
from django.views.generic.edit import View
from dashboard.mixins import DashboardView from dashboard.mixins import DashboardView
from evidence.models import Annotation from evidence.models import Annotation
from evidence.parse_details import ParseSnapshot
from evidence.parse import Build from evidence.parse import Build
from device.models import Device
from api.models import Token from api.models import Token
from api.tables import TokensTable from api.tables import TokensTable
logger = logging.getLogger('django') @csrf_exempt
def NewSnapshot(request):
# Accept only posts
if request.method != 'POST':
return JsonResponse({'error': 'Invalid request method'}, status=400)
# Authentication
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
token = auth_header.split(' ')[1]
tk = Token.objects.filter(token=token).first()
if not tk:
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
# Validation snapshot
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
# try:
# Build(data, None, check=True)
# except Exception:
# return JsonResponse({'error': 'Invalid Snapshot'}, status=400)
exist_annotation = Annotation.objects.filter(
uuid=data['uuid']
).first()
if exist_annotation:
txt = "error: the snapshot {} exist".format(data['uuid'])
return JsonResponse({'status': txt}, status=500)
# Process snapshot
path_name = save_in_disk(data, tk.owner.institution.name)
try:
Build(data, tk.owner)
except Exception as err:
return JsonResponse({'status': f"fail: {err}"}, status=500)
annotation = Annotation.objects.filter(
uuid=data['uuid'],
type=Annotation.Type.SYSTEM,
# TODO this is hardcoded, it should select the user preferred algorithm
key="hidalgo1",
owner=tk.owner.institution
).first()
class ApiMixing(View): if not annotation:
return JsonResponse({'status': 'fail'}, status=500)
@method_decorator(csrf_exempt) url_args = reverse_lazy("device:details", args=(annotation.value,))
def dispatch(self, *args, **kwargs): url = request.build_absolute_uri(url_args)
return super().dispatch(*args, **kwargs)
def auth(self): response = {
# Authentication "status": "success",
auth_header = self.request.headers.get('Authorization') "dhid": annotation.value[:6].upper(),
if not auth_header or not auth_header.startswith('Bearer '): "url": url,
logger.error("Invalid or missing token %s", auth_header) # TODO replace with public_url when available
return JsonResponse({'error': 'Invalid or missing token'}, status=401) "public_url": url
}
move_json(path_name, tk.owner.institution.name)
token = auth_header.split(' ')[1].strip("'").strip('"') return JsonResponse(response, status=200)
try:
uuid.UUID(token)
except Exception:
logger.error("Invalid or missing token %s", token)
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
self.tk = Token.objects.filter(token=token).first()
if not self.tk:
logger.error("Invalid or missing token %s", token)
return JsonResponse({'error': 'Invalid or missing token'}, status=401)
class NewSnapshotView(ApiMixing):
def get(self, request, *args, **kwargs):
return JsonResponse({}, status=404)
def post(self, request, *args, **kwargs):
response = self.auth()
if response:
return response
# Validation snapshot
try:
data = json.loads(request.body)
except json.JSONDecodeError:
txt = "error: the snapshot is not a json"
logger.error("%s", txt)
return JsonResponse({'error': 'Invalid JSON'}, status=500)
# Process snapshot
path_name = save_in_disk(data, self.tk.owner.institution.name)
# try:
# Build(data, None, check=True)
# except Exception:
# return JsonResponse({'error': 'Invalid Snapshot'}, status=400)
if not data.get("uuid"):
txt = "error: the snapshot not have uuid"
logger.error("%s", txt)
return JsonResponse({'status': txt}, status=500)
exist_annotation = Annotation.objects.filter(
uuid=data['uuid']
).first()
if exist_annotation:
txt = "error: the snapshot {} exist".format(data['uuid'])
logger.warning("%s", txt)
return JsonResponse({'status': txt}, status=500)
try:
Build(data, self.tk.owner)
except Exception as err:
if settings.DEBUG:
logger.exception("%s", err)
snapshot_id = data.get("uuid", "")
txt = "It is not possible to parse snapshot: %s."
logger.error(txt, snapshot_id)
text = "fail: It is not possible to parse snapshot"
return JsonResponse({'status': text}, status=500)
annotation = Annotation.objects.filter(
uuid=data['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 annotation:
logger.error("Error: No annotation for uuid: %s", data["uuid"])
return JsonResponse({'status': 'fail'}, status=500)
url_args = reverse_lazy("device:details", args=(annotation.value,))
url = request.build_absolute_uri(url_args)
response = {
"status": "success",
"dhid": annotation.value[:6].upper(),
"url": url,
# TODO replace with public_url when available
"public_url": url
}
move_json(path_name, self.tk.owner.institution.name)
return JsonResponse(response, status=200)
class TokenView(DashboardView, SingleTableView): class TokenView(DashboardView, SingleTableView):
@ -212,99 +165,3 @@ class EditTokenView(DashboardView, UpdateView):
) )
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
return kwargs return kwargs
class DetailsDeviceView(ApiMixing):
def get(self, request, *args, **kwargs):
response = self.auth()
if response:
return response
self.pk = kwargs['pk']
self.object = Device(id=self.pk)
if not self.object.last_evidence:
return JsonResponse({}, status=404)
if self.object.owner != self.tk.owner.institution:
return JsonResponse({}, status=403)
data = self.get_data()
return JsonResponse(data, status=200)
def post(self, request, *args, **kwargs):
return JsonResponse({}, status=404)
def get_data(self):
data = {}
self.object.initial()
self.object.get_last_evidence()
evidence = self.object.last_evidence
if evidence.is_legacy():
data.update({
"device": evidence.get("device"),
"components": evidence.get("components"),
})
else:
evidence.get_doc()
snapshot = ParseSnapshot(evidence.doc).snapshot_json
data.update({
"device": snapshot.get("device"),
"components": snapshot.get("components"),
})
uuids = Annotation.objects.filter(
owner=self.tk.owner.institution,
value=self.pk
).values("uuid")
annotations = Annotation.objects.filter(
uuid__in=uuids,
owner=self.tk.owner.institution,
type = Annotation.Type.USER
).values_list("key", "value")
data.update({"annotations": list(annotations)})
return data
class AddAnnotationView(ApiMixing):
def post(self, request, *args, **kwargs):
response = self.auth()
if response:
return response
self.pk = kwargs['pk']
institution = self.tk.owner.institution
self.annotation = Annotation.objects.filter(
owner=institution,
value=self.pk,
type=Annotation.Type.SYSTEM
).first()
if not self.annotation:
return JsonResponse({}, status=404)
try:
data = json.loads(request.body)
key = data["key"]
value = data["value"]
except Exception:
logger.error("Invalid Snapshot of user %s", self.tk.owner)
return JsonResponse({'error': 'Invalid JSON'}, status=500)
Annotation.objects.create(
uuid=self.annotation.uuid,
owner=self.tk.owner.institution,
type = Annotation.Type.USER,
key = key,
value = value
)
return JsonResponse({"status": "success"}, status=200)
def get(self, request, *args, **kwargs):
return JsonResponse({}, status=404)

View File

@ -66,21 +66,14 @@ class SearchView(InventaryMixin):
limit limit
) )
if not matches or not matches.size(): if not matches.size():
return self.search_hids(query, offset, limit) return self.search_hids(query, offset, limit)
devices = [] devices = []
dev_id = []
for x in matches: for x in matches:
# devices.append(self.get_annotations(x)) devices.append(self.get_annotations(x))
dev = self.get_annotations(x)
if dev.id not in dev_id:
devices.append(dev)
dev_id.append(dev.id)
count = matches.size() count = matches.size()
# TODO fix of pagination, the count is not correct
return devices, count return devices, count
def get_annotations(self, xp): def get_annotations(self, xp):

View File

@ -1,7 +1,8 @@
from django.db import models, connection from django.db import models, connection
from utils.constants import ALGOS from utils.constants import STR_SM_SIZE, STR_SIZE, STR_EXTEND_SIZE, ALGOS
from evidence.models import Annotation, Evidence from evidence.models import Annotation, Evidence
from user.models import User
from lot.models import DeviceLot from lot.models import DeviceLot
@ -109,22 +110,6 @@ class Device:
annotation = annotations.first() annotation = annotations.first()
self.last_evidence = Evidence(annotation.uuid) self.last_evidence = Evidence(annotation.uuid)
def is_eraseserver(self):
if not self.uuids:
self.get_uuids()
if not self.uuids:
return False
annotation = Annotation.objects.filter(
uuid__in=self.uuids,
owner=self.owner,
type=Annotation.Type.ERASE_SERVER
).first()
if annotation:
return True
return False
def last_uuid(self): def last_uuid(self):
return self.uuids[0] return self.uuids[0]

View File

@ -1,254 +1,216 @@
{% extends 'base.html' %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3>{{ object.shortid }}</h3> <h3>{{ object.shortid }}</h3>
</div>
</div>
<div class="row">
<div class="col">
<ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-items">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#details">General details</button>
</li>
<li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#annotations">User annotations</button>
</li>
<li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#documents">Documents</button>
</li>
<li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">Lots</button>
</li>
<li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#components">Components</button>
</li>
<li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">Evidences</button>
</li>
<li class="nav-items">
<a class="nav-link" href="">Web</a>
</li>
</ul>
</div>
</div>
<div class="tab-content pt-2">
<div class="tab-pane fade show active" id="details">
<h5 class="card-title">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> </div>
<div class="row">
<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 %}
{% for k, v in object.last_user_evidence %}
<div class="row">
<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">
<div class="col-lg-3 col-md-4 label">Manufacturer</div>
<div class="col-lg-9 col-md-8">{{ object.manufacturer|default:"" }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Model</div>
<div class="col-lg-9 col-md-8">{{ object.model|default:"" }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Serial Number</div>
<div class="col-lg-9 col-md-8">{{ object.last_evidence.doc.device.serialNumber|default:"" }}</div>
</div>
{% endif %}
<div class="row">
<div class="col-lg-3 col-md-4 label">Identifiers</div>
</div>
{% for chid in object.hids %}
<div class="row">
<div class="col">{{ chid |default:"" }}</div>
</div>
{% endfor %}
</div> </div>
<div class="row"> <div class="tab-pane fade profile-overview" id="annotations">
<div class="col"> <div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
<ul class="nav nav-tabs nav-tabs-bordered"> <a href="{% url 'device:add_annotation' object.pk %}" class="btn btn-primary">
<li class="nav-item"> <i class="bi bi-plus"></i>
<a href="#details" class="nav-link active" data-bs-toggle="tab" data-bs-target="#details">{% trans 'General details' %}</a> Add new annotation
</li> <span class="caret"></span>
<li class="nav-item"> </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>
</li>
<li class="nav-item">
<a href="#lots" class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">{% trans 'Lots' %}</a>
</li>
<li class="nav-item">
<a href="#components" class="nav-link" data-bs-toggle="tab" data-bs-target="#components">{% trans 'Components' %}</a>
</li>
<li class="nav-item">
<a href="#evidences" class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">{% trans 'Evidences' %}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'device:device_web' object.id %}" target="_blank">Web</a>
</li>
</ul>
</div> </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 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>
<div class="tab-content pt-2">
<div class="tab-pane fade show active" id="details"> <div class="tab-pane fade profile-overview" id="lots">
<h5 class="card-title">{% trans 'Details' %}</h5> {% for tag in lot_tags %}
<div class="row mb-3"> <h5 class="card-title">{{ tag }}</h5>
<div class="col-lg-3 col-md-4 label">Phid</div>
<div class="col-lg-9 col-md-8">{{ object.id }}</div> {% for lot in object.lots %}
{% if lot.type == tag %}
<div class="row">
<div class="col">
<a href="{% url 'dashboard:lot' lot.id %}">{{ lot.name }}</a>
</div> </div>
</div>
{% endif %}
{% endfor %}
{% endfor %}
</div>
{% if object.is_eraseserver %} <div class="tab-pane fade profile-overview" id="documents">
<div class="row mb-3"> <div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
<div class="col-lg-3 col-md-4 label"> <a href="{% url 'device:add_document' object.pk %}" class="btn btn-primary">
{% trans 'Is a erase server' %}
</div> <i class="bi bi-plus"></i>
<div class="col-lg-9 col-md-8"></div> Add new document
<span class="caret"></span>
</a>
</div>
<h5 class="card-title mt-2">Documents</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 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 profile-overview" id="components">
<h5 class="card-title">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> </div>
{% endif %} <p class="mb-1">
{% for k, v in c.items %}
<div class="row mb-3"> {% if k not in "actions,type" %}
<div class="col-lg-3 col-md-4 label">Type</div> {{ k }}: {{ v }}<br />
<div class="col-lg-9 col-md-8">{{ object.type }}</div> {% endif %}
{% endfor %}
<br />
</p>
<small class="text-muted">
</small>
</div> </div>
{% if object.is_websnapshot %}
{% for k, v in object.last_user_evidence %}
<div class="row mb-3">
<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-3">
<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-3">
<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-3">
<div class="col-lg-3 col-md-4 label">
{% trans 'Serial Number' %}
</div>
<div class="col-lg-9 col-md-8">{{ object.last_evidence.doc.device.serialNumber|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 %} {% endfor %}
</div> </div>
</div>
<div class="tab-pane fade" id="annotations"> <div class="tab-pane fade profile-overview" id="evidences">
<div class="btn-group mt-1 mb-3"> <h5 class="card-title">List of evidences</h5>
<a href="{% url 'device:add_annotation' object.pk %}" class="btn btn-primary"> <div class="list-group col-6">
<i class="bi bi-plus"></i> {% for snap in object.evidences %}
{% trans 'Add new annotation' %} <div class="list-group-item">
</a> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1"></h5>
<small class="text-muted">{{ snap.created }}</small>
</div>
<p class="mb-1">
<a href="{% url 'evidence:details' snap.uuid %}">{{ snap.uuid }}</a>
</p>
<small class="text-muted">
</small>
</div> </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 %} {% endfor %}
</div> </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>
</div> </div>
{% endblock %} </div>
{% block extrascript %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Obtener el hash de la URL (ejemplo: #components)
const hash = window.location.hash
// Verificar si hay un hash en la URL
if (hash) {
// Buscar el botón o enlace que corresponde al hash y activarlo
const tabTrigger = document.querySelector(`[data-bs-target="${hash}"]`)
if (tabTrigger) {
// Crear una instancia de tab de Bootstrap para activar el tab
const tab = new bootstrap.Tab(tabTrigger)
tab.show()
}
}
})
</script>
{% endblock %} {% endblock %}

View File

@ -1,169 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ object.type }}</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" />
<style>
body {
font-size: 0.875rem;
background-color: #f8f9fa;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.custom-container {
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 30px;
margin-top: 30px;
flex-grow: 1;
}
.section-title {
color: #7a9f4f;
border-bottom: 2px solid #9cc666;
padding-bottom: 10px;
margin-bottom: 20px;
font-size: 1.5em;
}
.info-row {
margin-bottom: 10px;
}
.info-label {
font-weight: bold;
color: #545f71;
}
.info-value {
color: #333;
}
.component-card {
background-color: #f8f9fa;
border-left: 4px solid #9cc666;
margin-bottom: 15px;
transition: all 0.3s ease;
}
.component-card:hover {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.hash-value {
word-break: break-all;
background-color: #f3f3f3;
padding: 5px;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
border: 1px solid #e0e0e0;
}
.card-title {
color: #9cc666;
}
.btn-primary {
background-color: #9cc666;
border-color: #9cc666;
padding: 0.1em 2em;
font-weight: 700;
}
.btn-primary:hover {
background-color: #8ab555;
border-color: #8ab555;
}
.btn-green-user {
background-color: #c7e3a3;
}
.btn-grey {
background-color: #f3f3f3;
}
footer {
background-color: #545f71;
color: #ffffff;
text-align: center;
padding: 10px 0;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container custom-container">
<h1 class="text-center mb-4" style="color: #545f71;">{{ object.manufacturer }} {{ object.type }} {{ object.model }}</h1>
<div class="row">
<div class="col-lg-6">
<h2 class="section-title">Details</h2>
<div class="info-row row">
<div class="col-md-4 info-label">Phid</div>
<div class="col-md-8 info-value">
<div class="hash-value">{{ object.id }}</div>
</div>
</div>
<div class="info-row row">
<div class="col-md-4 info-label">Type</div>
<div class="col-md-8 info-value">{{ object.type }}</div>
</div>
{% if object.is_websnapshot %}
{% for snapshot_key, snapshot_value in object.last_user_evidence %}
<div class="info-row row">
<div class="col-md-4 info-label">{{ snapshot_key }}</div>
<div class="col-md-8 info-value">{{ snapshot_value|default:'' }}</div>
</div>
{% endfor %}
{% else %}
<div class="info-row row">
<div class="col-md-4 info-label">Manufacturer</div>
<div class="col-md-8 info-value">{{ object.manufacturer|default:'' }}</div>
</div>
<div class="info-row row">
<div class="col-md-4 info-label">Model</div>
<div class="col-md-8 info-value">{{ object.model|default:'' }}</div>
</div>
<div class="info-row row">
<div class="col-md-4 info-label">Serial Number</div>
<div class="col-md-8 info-value">{{ object.last_evidence.doc.device.serialNumber|default:'' }}</div>
</div>
{% endif %}
</div>
<div class="col-lg-6">
<h2 class="section-title">Identifiers</h2>
{% for chid in object.hids %}
<div class="info-row">
<div class="hash-value">{{ chid|default:'' }}</div>
</div>
{% endfor %}
</div>
</div>
<h2 class="section-title mt-5">Components</h2>
<div class="row">
{% for component in object.components %}
<div class="col-md-6 mb-3">
<div class="card component-card">
<div class="card-body">
<h5 class="card-title">{{ component.type }}</h5>
<p class="card-text">
{% for component_key, component_value in component.items %}
{% if component_key not in 'actions,type' %}
<strong>{{ component_key }}:</strong> {{ component_value }}<br />
{% endif %}
{% endfor %}
</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<footer>
<p>
&copy;{% now 'Y' %} eReuse. All rights reserved.
</p>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
</body>
</html>

3
device/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,76 +0,0 @@
from device.models import Device
from unittest.mock import MagicMock
class TestDevice(Device):
"""A test subclass of Device that overrides the database-dependent methods"""
# TODO Leaving commented bc not used, but might be useful at some point
# def get_annotations(self):
# """Return empty list instead of querying database"""
# return []
# def get_uuids(self):
# """Set uuids directly instead of querying"""
# self.uuids = ['uuid1', 'uuid2']
# def get_hids(self):
# """Set hids directly instead of querying"""
# self.hids = ['hid1', 'hid2']
# def get_evidences(self):
# """Set evidences directly instead of querying"""
# self.evidences = []
# def get_lots(self):
# """Set lots directly instead of querying"""
# self.lots = []
def get_last_evidence(self):
if not hasattr(self, '_evidence'):
self._evidence = MagicMock()
self._evidence.doc = {
'type': 'Computer',
'manufacturer': 'Test Manufacturer',
'model': 'Test Model',
'device': {
'serialNumber': 'SN123456',
'type': 'Computer'
}
}
self._evidence.get_manufacturer = lambda: 'Test Manufacturer'
self._evidence.get_model = lambda: 'Test Model'
self._evidence.get_chassis = lambda: 'Computer'
self._evidence.get_components = lambda: [
{
'type': 'CPU',
'model': 'Intel i7',
'manufacturer': 'Intel'
},
{
'type': 'RAM',
'size': '8GB',
'manufacturer': 'Kingston'
}
]
self.last_evidence = self._evidence
class TestWebSnapshotDevice(TestDevice):
"""A test subclass of Device that simulates a WebSnapshot device"""
def get_last_evidence(self):
if not hasattr(self, '_evidence'):
self._evidence = MagicMock()
self._evidence.doc = {
'type': 'WebSnapshot',
'kv': {
'URL': 'http://example.com',
'Title': 'Test Page',
'Timestamp': '2024-01-01'
},
'device': {
'type': 'Laptop'
}
}
self.last_evidence = self._evidence
return self._evidence

View File

@ -1,67 +0,0 @@
from django.test import TestCase, Client
from django.urls import reverse
from unittest.mock import patch
from device.views import PublicDeviceWebView
from device.tests.test_mock_device import TestDevice, TestWebSnapshotDevice
class PublicDeviceWebViewTests(TestCase):
def setUp(self):
self.client = Client()
self.test_id = "test123"
self.test_url = reverse('device:device_web',
kwargs={'pk': self.test_id})
def test_url_resolves_correctly(self):
"""Test that the URL is constructed correctly"""
url = reverse('device:device_web', kwargs={'pk': self.test_id})
self.assertEqual(url, f'/device/{self.test_id}/public/')
@patch('device.views.Device')
def test_html_response(self, MockDevice):
test_device = TestDevice(id=self.test_id)
MockDevice.return_value = test_device
response = self.client.get(self.test_url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'device_web.html')
self.assertContains(response, 'Test Manufacturer')
self.assertContains(response, 'Test Model')
self.assertContains(response, 'Computer')
self.assertContains(response, self.test_id)
self.assertContains(response, 'CPU')
self.assertContains(response, 'Intel')
self.assertContains(response, 'RAM')
self.assertContains(response, 'Kingston')
@patch('device.views.Device')
def test_json_response(self, MockDevice):
test_device = TestDevice(id=self.test_id)
MockDevice.return_value = test_device
response = self.client.get(
self.test_url,
HTTP_ACCEPT='application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
json_data = response.json()
self.assertEqual(json_data['id'], self.test_id)
self.assertEqual(json_data['shortid'], self.test_id[:6].upper())
self.assertEqual(json_data['components'], test_device.components)
@patch('device.views.Device')
def test_websnapshot_device(self, MockDevice):
test_device = TestWebSnapshotDevice(id=self.test_id)
MockDevice.return_value = test_device
response = self.client.get(self.test_url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'device_web.html')
self.assertContains(response, 'http://example.com')
self.assertContains(response, 'Test Page')

View File

@ -9,6 +9,4 @@ urlpatterns = [
path("<str:pk>/", views.DetailsView.as_view(), name="details"), path("<str:pk>/", views.DetailsView.as_view(), name="details"),
path("<str:pk>/annotation/add", views.AddAnnotationView.as_view(), name="add_annotation"), 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>/document/add", views.AddDocumentView.as_view(), name="add_document"),
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web"),
] ]

View File

@ -1,5 +1,4 @@
import json import json
from django.http import JsonResponse
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -111,39 +110,6 @@ class DetailsView(DashboardView, TemplateView):
return context return context
class PublicDeviceWebView(TemplateView):
template_name = "device_web.html"
def get(self, request, *args, **kwargs):
self.pk = kwargs['pk']
self.object = Device(id=self.pk)
if not self.object.last_evidence:
raise Http404
if self.request.headers.get('Accept') == 'application/json':
return self.get_json_response()
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
self.object.initial()
context.update({
'object': self.object
})
return context
def get_json_response(self):
data = {
'id': self.object.id,
'shortid': self.object.shortid,
'uuids': self.object.uuids,
'hids': self.object.hids,
'components': self.object.components
}
return JsonResponse(data)
class AddAnnotationView(DashboardView, CreateView): class AddAnnotationView(DashboardView, CreateView):
template_name = "new_annotation.html" template_name = "new_annotation.html"
title = _("New annotation") title = _("New annotation")

View File

@ -17,8 +17,6 @@ from pathlib import Path
from django.contrib.messages import constants as messages from django.contrib.messages import constants as messages
from decouple import config, Csv from decouple import config, Csv
from utils.logger import CustomFormatter
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -34,6 +32,8 @@ DEBUG = config('DEBUG', default=False, cast=bool)
DOMAIN = config("DOMAIN") DOMAIN = config("DOMAIN")
assert DOMAIN not in [None, ''], "DOMAIN var is MANDATORY" assert DOMAIN not in [None, ''], "DOMAIN var is MANDATORY"
# this var is very important, we print it
print("DOMAIN: " + DOMAIN)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=DOMAIN, cast=Csv()) ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=DOMAIN, cast=Csv())
assert DOMAIN in ALLOWED_HOSTS, f"DOMAIN {DOMAIN} is not in ALLOWED_HOSTS {ALLOWED_HOSTS}" assert DOMAIN in ALLOWED_HOSTS, f"DOMAIN {DOMAIN} is not in ALLOWED_HOSTS {ALLOWED_HOSTS}"
@ -158,14 +158,11 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
TIME_ZONE = config("TIME_ZONE", default="UTC") TIME_ZONE = "UTC"
USE_I18N = True USE_I18N = True
USE_TZ = False USE_TZ = True
if TIME_ZONE == "UTC":
USE_TZ = True
USE_L10N = True USE_L10N = True
LANGUAGES = [ LANGUAGES = [
@ -205,34 +202,12 @@ LOGOUT_REDIRECT_URL = '/'
LOGGING = { LOGGING = {
"version": 1, "version": 1,
"disable_existing_loggers": False, "disable_existing_loggers": False,
'formatters': {
'colored': {
'()': CustomFormatter,
'format': '%(levelname)s %(asctime)s %(message)s'
},
},
"handlers": { "handlers": {
"console": { "console": {"level": "DEBUG", "class": "logging.StreamHandler"},
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "colored"
},
}, },
"root": { "root": {
"handlers": ["console"], "handlers": ["console"],
"level": "DEBUG", "level": "DEBUG",
},
"loggers": {
"django": {
"handlers": ["console"],
"level": "INFO",
"propagate": False, # Asegura que no se reenvíen a los manejadores raíz
},
"django.request": {
"handlers": ["console"],
"level": "ERROR",
"propagate": False,
}
} }
} }

View File

@ -4,7 +4,7 @@ services:
build: build:
dockerfile: docker/devicehub-django.Dockerfile dockerfile: docker/devicehub-django.Dockerfile
environment: environment:
- DEBUG=${DEBUG:-false} - DEBUG=true
- DOMAIN=${DOMAIN:-localhost} - DOMAIN=${DOMAIN:-localhost}
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-$DOMAIN} - ALLOWED_HOSTS=${ALLOWED_HOSTS:-$DOMAIN}
- DEMO=${DEMO:-false} - DEMO=${DEMO:-false}

View File

@ -18,8 +18,6 @@ deploy() {
if [ "${DEBUG:-}" = 'true' ]; then if [ "${DEBUG:-}" = 'true' ]; then
./manage.py print_settings ./manage.py print_settings
else
echo "DOMAIN: ${DOMAIN}"
fi fi
# detect if existing deployment (TODO only works with sqlite) # detect if existing deployment (TODO only works with sqlite)

View File

@ -162,56 +162,3 @@ class ImportForm(forms.Form):
return table return table
return return
class EraseServerForm(forms.Form):
erase_server = forms.BooleanField(label=_("Is a Erase Server"), required=False)
def __init__(self, *args, **kwargs):
self.pk = None
self.uuid = kwargs.pop('uuid', None)
self.user = kwargs.pop('user')
instance = Annotation.objects.filter(
uuid=self.uuid,
type=Annotation.Type.ERASE_SERVER,
key='ERASE_SERVER',
owner=self.user.institution
).first()
if instance:
kwargs["initial"]["erase_server"] = instance.value
self.pk = instance.pk
super().__init__(*args, **kwargs)
def clean(self):
self.erase_server = self.cleaned_data.get('erase_server', False)
self.instance = Annotation.objects.filter(
uuid=self.uuid,
type=Annotation.Type.ERASE_SERVER,
key='ERASE_SERVER',
owner=self.user.institution
).first()
return True
def save(self, user, commit=True):
if not commit:
return
if not self.erase_server:
if self.instance:
self.instance.delete()
return
if self.instance:
return
Annotation.objects.create(
uuid=self.uuid,
type=Annotation.Type.ERASE_SERVER,
key='ERASE_SERVER',
value=self.erase_server,
owner=self.user.institution,
user=self.user
)

View File

@ -1,83 +0,0 @@
import os
import json
import logging
from django.core.management.base import BaseCommand
from django.conf import settings
from utils.device import create_annotation, create_doc, create_index
from user.models import Institution
from evidence.parse import Build
logger = logging.getLogger('django')
class Command(BaseCommand):
help = "Reindex snapshots"
snapshots = []
EVIDENCES = settings.EVIDENCES_DIR
def handle(self, *args, **kwargs):
if os.path.isdir(self.EVIDENCES):
self.read_files(self.EVIDENCES)
self.parsing()
def read_files(self, directory):
for filename in os.listdir(directory):
filepath = os.path.join(directory, filename)
if not os.path.isdir(filepath):
continue
institution = Institution.objects.filter(name=filename).first()
if not institution:
continue
user = institution.user_set.filter(is_admin=True).first()
if not user:
txt = "No there are Admins for the institution: %s"
logger.warning(txt, institution.name)
continue
snapshots_path = os.path.join(filepath, "snapshots")
placeholders_path = os.path.join(filepath, "placeholders")
for f in os.listdir(snapshots_path):
f_path = os.path.join(snapshots_path, f)
if f_path[-5:] == ".json" and os.path.isfile(f_path):
self.open(f_path, user)
for f in os.listdir(placeholders_path):
f_path = os.path.join(placeholders_path, f)
if f_path[-5:] == ".json" and os.path.isfile(f_path):
self.open(f_path, user)
def open(self, filepath, user):
with open(filepath, 'r') as file:
content = json.loads(file.read())
self.snapshots.append((content, user, filepath))
def parsing(self):
for s, user, f_path in self.snapshots:
if s.get("type") == "Websnapshot":
self.build_placeholder(s, user, f_path)
else:
self.build_snapshot(s, user, f_path)
def build_placeholder(self, s, user, f_path):
try:
create_index(s, user)
create_annotation(s, user, commit=True)
except Exception as err:
txt = "In placeholder %s \n%s"
logger.warning(txt, f_path, err)
def build_snapshot(self, s, user, f_path):
try:
Build(s, user)
except Exception:
txt = "Error: in Snapshot {}".format(f_path)
logger.error(txt)

View File

@ -1,18 +1,12 @@
import os import os
import json import json
import logging
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.conf import settings
from utils.save_snapshots import move_json, save_in_disk
from evidence.parse import Build from evidence.parse import Build
logger = logging.getLogger('django')
User = get_user_model() User = get_user_model()
@ -49,15 +43,8 @@ class Command(BaseCommand):
def open(self, filepath): def open(self, filepath):
with open(filepath, 'r') as file: with open(filepath, 'r') as file:
content = json.loads(file.read()) content = json.loads(file.read())
path_name = save_in_disk(content, self.user.institution.name) self.snapshots.append(content)
self.snapshots.append((content, path_name))
def parsing(self): def parsing(self):
for s, p in self.snapshots: for s in self.snapshots:
try: self.devices.append(Build(s, self.user))
self.devices.append(Build(s, self.user))
move_json(p, self.user.institution.name)
except Exception as err:
snapshot_id = s.get("uuid", "")
txt = "Could not parse snapshot: %s"
logger.error(txt, snapshot_id)

View File

@ -1,25 +0,0 @@
# Generated by Django 5.0.6 on 2024-10-28 12:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("evidence", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="annotation",
name="type",
field=models.SmallIntegerField(
choices=[
(0, "System"),
(1, "User"),
(2, "Document"),
(3, "EraseServer"),
]
),
),
]

View File

@ -3,7 +3,7 @@ import json
from dmidecode import DMIParse from dmidecode import DMIParse
from django.db import models from django.db import models
from utils.constants import STR_EXTEND_SIZE, CHASSIS_DH from utils.constants import STR_SM_SIZE, STR_EXTEND_SIZE, CHASSIS_DH
from evidence.xapian import search from evidence.xapian import search
from evidence.parse_details import ParseSnapshot from evidence.parse_details import ParseSnapshot
from user.models import User, Institution from user.models import User, Institution
@ -14,7 +14,6 @@ class Annotation(models.Model):
SYSTEM= 0, "System" SYSTEM= 0, "System"
USER = 1, "User" USER = 1, "User"
DOCUMENT = 2, "Document" DOCUMENT = 2, "Document"
ERASE_SERVER = 3, "EraseServer"
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
uuid = models.UUIDField() uuid = models.UUIDField()
@ -62,13 +61,13 @@ class Evidence:
self.get_owner() self.get_owner()
qry = 'uuid:"{}"'.format(self.uuid) qry = 'uuid:"{}"'.format(self.uuid)
matches = search(self.owner, qry, limit=1) matches = search(self.owner, qry, limit=1)
if matches and matches.size() < 0: if matches.size() < 0:
return return
for xa in matches: for xa in matches:
self.doc = json.loads(xa.document.get_data()) self.doc = json.loads(xa.document.get_data())
if not self.is_legacy(): if self.doc.get("software") == "EreuseWorkbench":
dmidecode_raw = self.doc["data"]["dmidecode"] dmidecode_raw = self.doc["data"]["dmidecode"]
self.dmi = DMIParse(dmidecode_raw) self.dmi = DMIParse(dmidecode_raw)
@ -81,7 +80,7 @@ class Evidence:
self.created = self.annotations.last().created self.created = self.annotations.last().created
def get_components(self): def get_components(self):
if self.is_legacy(): if self.doc.get("software") != "EreuseWorkbench":
return self.doc.get('components', []) return self.doc.get('components', [])
self.set_components() self.set_components()
return self.components return self.components
@ -93,7 +92,7 @@ class Evidence:
return "" return ""
return list(self.doc.get('kv').values())[0] return list(self.doc.get('kv').values())[0]
if self.is_legacy(): if self.doc.get("software") != "EreuseWorkbench":
return self.doc['device']['manufacturer'] return self.doc['device']['manufacturer']
return self.dmi.manufacturer().strip() return self.dmi.manufacturer().strip()
@ -105,13 +104,13 @@ class Evidence:
return "" return ""
return list(self.doc.get('kv').values())[1] return list(self.doc.get('kv').values())[1]
if self.is_legacy(): if self.doc.get("software") != "EreuseWorkbench":
return self.doc['device']['model'] return self.doc['device']['model']
return self.dmi.model().strip() return self.dmi.model().strip()
def get_chassis(self): def get_chassis(self):
if self.is_legacy(): if self.doc.get("software") != "EreuseWorkbench":
return self.doc['device']['model'] return self.doc['device']['model']
chassis = self.dmi.get("Chassis")[0].get("Type", '_virtual') chassis = self.dmi.get("Chassis")[0].get("Type", '_virtual')
@ -128,11 +127,8 @@ class Evidence:
owner=user.institution, owner=user.institution,
type=Annotation.Type.SYSTEM, type=Annotation.Type.SYSTEM,
key="hidalgo1", key="hidalgo1",
).order_by("-created").values_list("uuid", "created").distinct() ).order_by("-created").values_list("uuid", flat=True).distinct()
def set_components(self): def set_components(self):
snapshot = ParseSnapshot(self.doc).snapshot_json snapshot = ParseSnapshot(self.doc).snapshot_json
self.components = snapshot['components'] self.components = snapshot['components']
def is_legacy(self):
return self.doc.get("software") != "workbench-script"

View File

@ -1,16 +1,13 @@
import os
import json import json
import shutil
import hashlib import hashlib
import logging
from datetime import datetime
from dmidecode import DMIParse from dmidecode import DMIParse
from json_repair import repair_json
from evidence.models import Annotation from evidence.models import Annotation
from evidence.xapian import index from evidence.xapian import index
from utils.constants import CHASSIS_DH from utils.constants import ALGOS, CHASSIS_DH
logger = logging.getLogger('django')
def get_network_cards(child, nets): def get_network_cards(child, nets):
@ -23,17 +20,9 @@ def get_network_cards(child, nets):
def get_mac(lshw): def get_mac(lshw):
nets = [] nets = []
try: try:
if type(lshw) is dict: get_network_cards(json.loads(lshw), nets)
hw = lshw
else:
hw = json.loads(lshw)
except json.decoder.JSONDecodeError:
hw = json.loads(repair_json(lshw))
try:
get_network_cards(hw, nets)
except Exception as ss: except Exception as ss:
logger.warning("%s", ss) print("WARNING!! {}".format(ss))
return return
nets_sorted = sorted(nets, key=lambda x: x['businfo']) nets_sorted = sorted(nets, key=lambda x: x['businfo'])
@ -68,7 +57,7 @@ class Build:
} }
def get_hid_14(self): def get_hid_14(self):
if self.json.get("software") == "workbench-script": if self.json.get("software") == "EreuseWorkbench":
hid = self.get_hid(self.json) hid = self.get_hid(self.json)
else: else:
device = self.json['device'] device = self.json['device']
@ -79,20 +68,9 @@ class Build:
sku = device.get("sku", '') sku = device.get("sku", '')
hid = f"{manufacturer}{model}{chassis}{serial_number}{sku}" hid = f"{manufacturer}{model}{chassis}{serial_number}{sku}"
return hashlib.sha3_256(hid.encode()).hexdigest() return hashlib.sha3_256(hid.encode()).hexdigest()
def create_annotations(self): 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.algorithms.items(): for k, v in self.algorithms.items():
Annotation.objects.create( Annotation.objects.create(
@ -135,7 +113,9 @@ class Build:
# mac = get_mac2(hwinfo_raw) or "" # mac = get_mac2(hwinfo_raw) or ""
mac = get_mac(lshw) or "" mac = get_mac(lshw) or ""
if not mac: if not mac:
txt = "Could not retrieve MAC address in snapshot %s" print(f"WARNING: Could not retrieve MAC address in snapshot {snapshot['uuid']}" )
logger.warning(txt, snapshot['uuid']) # TODO generate system annotation for that snapshot
else:
print(f"{manufacturer}{model}{chassis}{serial_number}{sku}{mac}")
return f"{manufacturer}{model}{chassis}{serial_number}{sku}{mac}" return f"{manufacturer}{model}{chassis}{serial_number}{sku}{mac}"

View File

@ -1,17 +1,11 @@
import json import json
import logging
import numpy as np import numpy as np
from datetime import datetime from datetime import datetime
from dmidecode import DMIParse from dmidecode import DMIParse
from json_repair import repair_json
from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE
logger = logging.getLogger('django')
def get_lshw_child(child, nets, component): def get_lshw_child(child, nets, component):
if child.get('id') == component: if child.get('id') == component:
nets.append(child) nets.append(child)
@ -166,7 +160,6 @@ class ParseSnapshot:
continue continue
model = sm.get('model_name') model = sm.get('model_name')
manufacturer = None manufacturer = None
hours = sm.get("power_on_time", {}).get("hours", 0)
if model and len(model.split(" ")) > 1: if model and len(model.split(" ")) > 1:
mm = model.split(" ") mm = model.split(" ")
model = mm[-1] model = mm[-1]
@ -182,7 +175,6 @@ class ParseSnapshot:
"size": self.get_data_storage_size(sm), "size": self.get_data_storage_size(sm),
"variant": sm.get("firmware_version"), "variant": sm.get("firmware_version"),
"interface": self.get_data_storage_interface(sm), "interface": self.get_data_storage_interface(sm),
"hours": hours,
} }
) )
@ -486,13 +478,9 @@ class ParseSnapshot:
def loads(self, x): def loads(self, x):
if isinstance(x, str): if isinstance(x, str):
try: try:
try: return json.loads(x)
hw = json.loads(x)
except json.decoder.JSONDecodeError:
hw = json.loads(repair_json(x))
return hw
except Exception as ss: except Exception as ss:
logger.warning("%s", ss) print("WARNING!! {}".format(ss))
return {} return {}
return x return x
@ -501,5 +489,5 @@ class ParseSnapshot:
return self._errors return self._errors
logger.error(txt) logger.error(txt)
self._errors.append("%s", txt) self._errors.append(txt)

View File

@ -12,16 +12,13 @@
<div class="col"> <div class="col">
<ul class="nav nav-tabs nav-tabs-bordered"> <ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-items"> <li class="nav-items">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#device">{% trans "Devices" %}</button> <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#device">Devices</button>
</li> </li>
<li class="nav-items"> <li class="nav-items">
<a href="#tag" class="nav-link" data-bs-toggle="tab" data-bs-target="#tag">{% trans "Tag" %}</a> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#tag">Tag</button>
</li>
<li class="nav-items"> <li class="nav-items">
<a href="{% url 'evidence:erase_server' object.uuid %}" class="nav-link">{% trans "Erase Server" %}</a> <a href="{% url 'evidence:download' object.uuid %}" class="nav-link">Download File</a>
</li> </li>
<li class="nav-items">
<a href="{% url 'evidence:download' object.uuid %}" class="nav-link">{% trans "Download File" %}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -29,44 +26,26 @@
<div class="tab-content pt-2"> <div class="tab-content pt-2">
<div class="tab-pane fade show active" id="device"> <div class="tab-pane fade show active" id="device">
<h5 class="card-title"></h5> <h5 class="card-title">List of chids</h5>
<div class="list-group col-6"> <div class="list-group col-6">
<table class="table"> {% for snap in object.annotations %}
<thead> {% if snap.type == 0 %}
<tr> <div class="list-group-item">
<th scope="col" data-sortable=""> <div class="d-flex w-100 justify-content-between">
{% trans "Type" %} <h5 class="mb-1"></h5>
</th> <small class="text-muted">
<th scope="col" data-sortable=""> {{ snap.created }}
{% trans "Identificator" %} </small>
</th> </div>
<th scope="col" data-sortable=""> <p class="mb-1">
{% trans "Data" %} {{ snap.key }}<br />
</th> </p>
</tr> <small class="text-muted">
</thead> <a href="{% url 'device:details' snap.value %}">{{ snap.value }}</a>
{% for snap in object.annotations %} </small>
<tbody> </div>
{% if snap.type == 0 %} {% endif %}
<tr> {% endfor %}
<td>
{{ snap.key }}
</td>
<td>
<small class="text-muted">
<a href="{% url 'device:details' snap.value %}">{{ snap.value }}</a>
</small>
</td>
<td>
<small class="text-muted">
{{ snap.created }}
</small>
</td>
</tr>
{% endif %}
</tbody>
{% endfor %}
</table>
</div> </div>
</div> </div>
<div class="tab-pane fade" id="tag"> <div class="tab-pane fade" id="tag">
@ -104,24 +83,3 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extrascript %}
<script>
document.addEventListener("DOMContentLoaded", function() {
// Obtener el hash de la URL (ejemplo: #components)
const hash = window.location.hash;
// Verificar si hay un hash en la URL
if (hash) {
// Buscar el botón o enlace que corresponde al hash y activarlo
const tabTrigger = document.querySelector(`[data-bs-target="${hash}"]`);
if (tabTrigger) {
// Crear una instancia de tab de Bootstrap para activar el tab
const tab = new bootstrap.Tab(tabTrigger);
tab.show();
}
}
});
</script>
{% endblock %}

View File

@ -1,61 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col">
<h3>{{ object.id }}</h3>
</div>
</div>
<div class="row">
<div class="col">
<ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-items">
<a href="{% url 'evidence:details' object.uuid %}" class="nav-link">{% trans "Devices" %}</a>
</li>
<li class="nav-items">
<a href="{% url 'evidence:details' object.uuid %}#tag" class="nav-link">{% trans "Tag" %}</a>
</li>
<li class="nav-items">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#erase_server">{% trans "Erase Server" %}</button>
</li>
<li class="nav-items">
<a href="{% url 'evidence:download' object.uuid %}" class="nav-link">{% trans "Download File" %}</a>
</li>
</ul>
</div>
</div>
<div class="tab-content pt-2">
<div class="tab-pane fade show active" id="erase_server">
{% load django_bootstrap5 %}
<div class="list-group col-6">
<form role="form" method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
<div class="message">
{% for field, error in form.errors.items %}
{{ error }}<br />
{% endfor %}
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
</div>
</div>
{% endif %}
{% bootstrap_form form %}
<div class="container">
<div class="row">
<div class="col">
<a class="btn btn-grey" href="">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -14,13 +14,10 @@
{% for ev in evidences %} {% for ev in evidences %}
<tr> <tr>
<td> <td>
<a href="{% url 'evidence:details' ev.0 %}">{{ ev.0 }}</a> <a href="{% url 'evidence:details' ev %}">{{ ev }}</a>
</td> </td>
<td> <td>
<small class="text-muted">{{ ev.1 }}</small> <a href="{# url 'evidence:delete' ev #}"><i class="bi bi-trash text-danger"></i></a>
</td>
<td>
<a href="{# url 'evidence:delete' ev.0 #}"><i class="bi bi-trash text-danger"></i></a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -18,7 +18,6 @@ urlpatterns = [
path("upload", views.UploadView.as_view(), name="upload"), path("upload", views.UploadView.as_view(), name="upload"),
path("import", views.ImportView.as_view(), name="import"), path("import", views.ImportView.as_view(), name="import"),
path("<uuid:pk>", views.EvidenceView.as_view(), name="details"), 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("<uuid:pk>/download", views.DownloadEvidenceView.as_view(), name="download"),
path('annotation/<int:pk>/del', views.AnnotationDeleteView.as_view(), name='delete_annotation'), path('annotation/<int:pk>/del', views.AnnotationDeleteView.as_view(), name='delete_annotation'),
] ]

View File

@ -11,14 +11,18 @@ from django.views.generic.edit import (
FormView, FormView,
) )
from dashboard.mixins import DashboardView, Http403 from dashboard.mixins import DashboardView, Http403
from evidence.models import Evidence, Annotation from evidence.models import Evidence, Annotation
from evidence.forms import ( from evidence.forms import UploadForm, UserTagForm, ImportForm
UploadForm, # from django.shortcuts import render
UserTagForm, # from rest_framework import viewsets
ImportForm, # from snapshot.serializers import SnapshotSerializer
EraseServerForm
)
# class SnapshotViewSet(viewsets.ModelViewSet):
# queryset = Snapshot.objects.all()
# serializer_class = SnapshotSerializer
class ListEvidencesView(DashboardView, TemplateView): class ListEvidencesView(DashboardView, TemplateView):
@ -163,48 +167,3 @@ class AnnotationDeleteView(DashboardView, DeleteView):
return redirect(url_name, **kwargs_view) return redirect(url_name, **kwargs_view)
class EraseServerView(DashboardView, FormView):
template_name = "ev_eraseserver.html"
section = "evidences"
title = _("Evidences")
breadcrumb = "Evidences / Details"
success_url = reverse_lazy('evidence:list')
form_class = EraseServerForm
def get(self, request, *args, **kwargs):
self.pk = kwargs['pk']
self.object = Evidence(self.pk)
if self.object.owner != self.request.user.institution:
raise Http403
self.object.get_annotations()
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'object': self.object,
})
return context
def get_form_kwargs(self):
self.pk = self.kwargs.get('pk')
kwargs = super().get_form_kwargs()
kwargs['uuid'] = self.pk
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
form.save(self.request.user)
response = super().form_valid(form)
return response
def form_invalid(self, form):
response = super().form_invalid(form)
return response
def get_success_url(self):
success_url = reverse_lazy('evidence:details', args=[self.pk])
return success_url

View File

@ -11,10 +11,7 @@ import xapian
def search(institution, qs, offset=0, limit=10): def search(institution, qs, offset=0, limit=10):
try: database = xapian.Database("db")
database = xapian.Database("db")
except (xapian.DatabaseNotFoundError, xapian.DatabaseOpeningError):
return
qp = xapian.QueryParser() qp = xapian.QueryParser()
qp.set_database(database) qp.set_database(database)
@ -34,9 +31,12 @@ def search(institution, qs, offset=0, limit=10):
def index(institution, uuid, snap): def index(institution, uuid, snap):
uuid = 'uuid:"{}"'.format(uuid) uuid = 'uuid:"{}"'.format(uuid)
matches = search(institution, uuid, limit=1) try:
if matches and matches.size() > 0: matches = search(institution, uuid, limit=1)
return if matches.size() > 0:
return
except (xapian.DatabaseNotFoundError, xapian.DatabaseOpeningError):
pass
database = xapian.WritableDatabase("db", xapian.DB_CREATE_OR_OPEN) database = xapian.WritableDatabase("db", xapian.DB_CREATE_OR_OPEN)
indexer = xapian.TermGenerator() indexer = xapian.TermGenerator()

View File

@ -110,6 +110,7 @@
<script src="/static/js/bootstrap.bundle.min.js"></script> <script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script>
<script src="/static/js/dashboard.js"></script>
<script> <script>
const togglePassword = document.querySelector('#togglePassword'); const togglePassword = document.querySelector('#togglePassword');
const password = document.querySelector('#id_password'); const password = document.querySelector('#id_password');

View File

@ -1,18 +0,0 @@
# Generated by Django 5.0.6 on 2024-10-28 12:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("lot", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="lot",
name="closed",
field=models.BooleanField(default=False),
),
]

View File

@ -31,7 +31,7 @@ class Lot(models.Model):
name = models.CharField(max_length=STR_SIZE, blank=True, null=True) name = models.CharField(max_length=STR_SIZE, blank=True, null=True)
code = models.CharField(max_length=STR_SIZE, blank=True, null=True) code = models.CharField(max_length=STR_SIZE, blank=True, null=True)
description = models.CharField(max_length=STR_SIZE, blank=True, null=True) description = models.CharField(max_length=STR_SIZE, blank=True, null=True)
closed = models.BooleanField(default=False) closed = models.BooleanField(default=True)
owner = models.ForeignKey(Institution, 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) user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
type = models.ForeignKey(LotTag, on_delete=models.CASCADE) type = models.ForeignKey(LotTag, on_delete=models.CASCADE)

View File

@ -7,16 +7,6 @@
<h3>{{ subtitle }}</h3> <h3>{{ subtitle }}</h3>
</div> </div>
<div class="col text-center"> <div class="col text-center">
{% if show_closed %}
<a href="?show_closed=false" class="btn btn-green-admin">
{% trans 'Hide closed lots' %}
</a>
{% else %}
<a href="?show_closed=true" class="btn btn-green-admin">
{% trans 'Show closed lots' %}
</a>
{% endif %}
<a href="{% url 'lot:add' %}" type="button" class="btn btn-green-admin"> <a href="{% url 'lot:add' %}" type="button" class="btn btn-green-admin">
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
{% trans 'Add new lot' %} {% trans 'Add new lot' %}

View File

@ -131,13 +131,11 @@ class LotsTagsView(DashboardView, TemplateView):
tag = get_object_or_404(LotTag, owner=self.request.user.institution, id=self.pk) tag = get_object_or_404(LotTag, owner=self.request.user.institution, id=self.pk)
self.title += " {}".format(tag.name) self.title += " {}".format(tag.name)
self.breadcrumb += " {}".format(tag.name) self.breadcrumb += " {}".format(tag.name)
show_closed = self.request.GET.get('show_closed', 'false') == 'true' lots = Lot.objects.filter(owner=self.request.user.institution).filter(type=tag)
lots = Lot.objects.filter(owner=self.request.user.institution).filter(type=tag, closed=show_closed)
context.update({ context.update({
'lots': lots, 'lots': lots,
'title': self.title, 'title': self.title,
'breadcrumb': self.breadcrumb, 'breadcrumb': self.breadcrumb
'show_closed': show_closed
}) })
return context return context

View File

@ -10,4 +10,4 @@ pandas==2.2.2
xlrd==2.0.1 xlrd==2.0.1
odfpy==1.4.1 odfpy==1.4.1
pytz==2024.2 pytz==2024.2
json-repair==0.30.0

View File

@ -2,17 +2,12 @@ import json
import uuid import uuid
import hashlib import hashlib
import datetime import datetime
import logging
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from evidence.xapian import index from evidence.xapian import index
from evidence.models import Annotation from evidence.models import Annotation
from device.models import Device from device.models import Device
logger = logging.getLogger('django')
def create_doc(data): def create_doc(data):
if not data: if not data:
return return
@ -81,17 +76,6 @@ def create_annotation(doc, user, commit=False):
'value': doc['CUSTOMER_ID'], 'value': doc['CUSTOMER_ID'],
} }
if commit: if commit:
annotation = Annotation.objects.filter(
uuid=doc["uuid"],
owner=user.institution,
type=Annotation.Type.SYSTEM,
)
if annotation:
txt = "Warning: Snapshot %s already registered (annotation exists)"
logger.warning(txt, doc["uuid"])
return annotation
return Annotation.objects.create(**data) return Annotation.objects.create(**data)
return Annotation(**data) return Annotation(**data)

View File

@ -1,37 +0,0 @@
import logging
from django.conf import settings
# Colors
RED = "\033[91m"
PURPLE = "\033[95m"
YELLOW = "\033[93m"
RESET = "\033[0m"
class CustomFormatter(logging.Formatter):
def format(self, record):
if record.levelname == "ERROR":
color = RED
elif record.levelname == "WARNING":
color = PURPLE
elif record.levelname in ["INFO", "DEBUG"]:
color = YELLOW
else:
color = RESET
record.levelname = f"{color}{record.levelname}{RESET}"
if record.args:
record.msg = self.highlight_args(record.msg, record.args, color)
record.args = ()
# provide trace when DEBUG config
if settings.DEBUG:
import traceback
print(traceback.format_exc())
return super().format(record)
def highlight_args(self, message, args, color):
highlighted_args = tuple(f"{color}{arg}{RESET}" for arg in args)
return message % highlighted_args