Compare commits

..

244 commits

Author SHA1 Message Date
Cayo Puigdefabregas bb8be7cc09 fix hid for legacy 2025-02-14 17:10:47 +01:00
Cayo Puigdefabregas 427e80f8b3 fix hid 2025-02-14 17:01:43 +01:00
pedro 42f5cf7e36 Merge pull request 'bugfix integration with dpp/dlt' (#50) from fix_build into main
Reviewed-on: #50
2025-02-14 13:36:39 +00:00
Cayo Puigdefabregas 73a582aeb3 fix drop actions 2025-02-14 14:25:54 +01:00
Cayo Puigdefabregas 28479fb871 fix linkg in details -> dpp 2025-02-14 12:06:36 +01:00
Cayo Puigdefabregas 0983af929a fix same chid for diferent algos 2 2025-02-14 11:42:59 +01:00
Cayo Puigdefabregas 49cce3daa9 fix same chid for diferent algos 2025-02-14 11:42:59 +01:00
Cayo Puigdefabregas e23fed5c13 fix last_dpp 2025-02-14 11:42:59 +01:00
pedro f54dce0979 devicehub docker entrypoint: cleanup old code 2025-02-13 18:29:30 +01:00
Cayo Puigdefabregas 8edfaa1bc5 fix get did from chir and add dpp template for details of device 2025-02-13 11:43:46 +01:00
Cayo Puigdefabregas 74d48c173b fix dev.build 2025-02-12 17:00:34 +01:00
Cayo Puigdefabregas f91818e515 fix de correct device in document 2025-02-12 12:18:30 +01:00
Cayo Puigdefabregas 8d37932aa0 fix document 2025-02-12 10:18:43 +01:00
Cayo Puigdefabregas 0903f53f8b fix get_doc with build.get_doc 2025-02-12 09:20:14 +01:00
Cayo Puigdefabregas ccaa8834dc fix bug 2025-02-11 19:50:31 +01:00
pedro bd1efe3adc docker compose: bugfix wrong use of tags 2025-02-11 19:49:34 +01:00
pedro 35a753d244 docker: better devicehub entrypoint 2025-02-11 16:15:49 +01:00
pedro 976ce43e6e Merge pull request 'redefine_algorithm_names' (#48) from redefine_algorithm_names into main
Reviewed-on: #48
2025-02-10 12:18:49 +00:00
Cayo Puigdefabregas c540bb7f7f redefine algo names as hidalgo1 for ereuse24 2025-02-10 13:16:32 +01:00
pedro 7145e721f1 Merge pull request 'Properties rework, States, StatesDefinitions, DeviceLog, and Notes' (#37) from feature/states into main
Reviewed-on: #37
2025-02-10 12:05:57 +00:00
Cayo Puigdefabregas 3db94ee82b fix more than one snapshot 2025-01-31 17:35:41 +01:00
Cayo Puigdefabregas 5cf51df952 fix rebase with main 2025-01-31 17:11:51 +01:00
Cayo Puigdefabregas e7d958c550 fix lot properties and clean absurd code from chatgpt 2025-01-31 16:53:29 +01:00
Cayo Puigdefabregas 2e932b9725 fix redirect correctly from buttons 2025-01-31 16:53:29 +01:00
Cayo Puigdefabregas a4fb574d07 fix error editing states 2025-01-31 16:53:29 +01:00
Cayo Puigdefabregas a52ce5c889 clean apps unused like documents 2025-01-31 16:53:29 +01:00
Cayo Puigdefabregas f9b93d4790 rebuild details template 2025-01-31 16:53:29 +01:00
Cayo Puigdefabregas 00df679156 fix rebase in parse 2025-01-31 16:53:29 +01:00
pedro 0259c2be2c tests: clarification on error/test 2025-01-31 16:53:29 +01:00
pedro a496045f4d add .editorconfig 2025-01-31 16:53:29 +01:00
pedro 73bb3b4751 e2e: progress 2025-01-31 16:53:29 +01:00
pedro 0db033e2dd init end-to-end tests with playwright 2025-01-31 16:53:29 +01:00
Cayo Puigdefabregas b58cfc9ab1 fix base template 2025-01-31 16:52:12 +01:00
Cayo Puigdefabregas 1cb09fb19f split long line 2025-01-31 16:52:12 +01:00
Cayo Puigdefabregas 9565288eee add version 2025-01-31 16:52:12 +01:00
Cayo Puigdefabregas 8b9ea1cf10 replace waring for yellow in details 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki ad497e6299 deleting obsolete if statement 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki c383e692c3 renamed property variable to prop 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 4da9961eea added missing components tab 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki c0c4e29fdc added logging for evidence tag changes 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 0a50f75ca4 text size adjustment and displaying none states 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 8136684b91 more contrast on save note button 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 67d7621509 states and notes view refactoring 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 86a7d9f733 userproperties views refactoring 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki a06cf9f4da better bootstrap tables 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki a0596f618b view changes 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki afb7cf8d6e better representiation of delete/edit notes 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki df386136ce edit update cannot be blank 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki b1b9f7e100 adding remove button for notes 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 8f0b8771a7 notes now can be updated 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 11813db7f6 deleted undo state view 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 86c2a26130 added a sidebar notes display 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 6b7fd09777 changes to state defiinitions list 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 643e6c1f45 better success message and removed devicelog var 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 2837d3e560 normalized deviceLog messages 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 3bed441d10 command for adding default states 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 0fb3df0155 erased old logger 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 3e2a5f03bf minor cosmetic changes 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 1f4515781a state definition list changes and disabled logging 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki a1381f68fa default value for no state 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki fcc93955c4 adding orm migrations 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 455448aea2 log list now shows log table 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki afcb6feb22 added logging for states, user properties and notes 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 0ca4a83f97 simpler state change and action input 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki c93915f285 current state table erased and spacing fix 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 2b0f0b8d08 deleted unique constraint on userproperty 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 18ec5d74c9 notes and log models added 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 63c427e3eb device tag bugfix 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 5a5dfc3319 lotproperties delete and update added 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 519648226b added action migration 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 24f5508462 now check for state on delete modal 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 0d8ec72d7c cosmetic changes to statesdefinitions list 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki d75cd75c86 change state delete to undo last state 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 85013a340e help icon added and new icon on add button 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 24753b1004 minor changes to states view 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 2dfe313076 statedefinitions update and delete popup changes 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 3db4374cd0 changes to state definitions list 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki cf48000ef4 statedefinitions edit popup added and url changes 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 30826afd45 state button rework and warning if same state 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki c530454054 current state helper function added 2025-01-31 16:52:12 +01:00
Thomas Nahuel Rusiecki 6e88f77c1b Device-details html modularized into several files 2025-01-31 16:52:11 +01:00
Thomas Nahuel Rusiecki 70175be472 statedefinitions list updated 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki dbd837b079 updated logging for states 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 7a55501c34 statedefinitions delete now uses correct id 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki d92dec28bb logging for statedefinitions 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 3467a483e7 better delete modal 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 5a448755c2 erase_server type now userproperty 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki f3f18e0962 deleted obsolete field type for sysproperties 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 080ff4f668 updated views for new model structure 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki ec9ae644b9 more property model refactoring 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki b5c57aa4d2 property models refactor 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 68d8ff33a7 added logging for user property creation 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 3cf6af0c25 made log var a env variable 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 4d38e75bba added loggin for state actions 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 5c7db7d60d added state delete 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 4045baac9f changed tab to log and added logging 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 0e3bba0569 changed state visual and ordering 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 012e25d086 minor fix for correct tab handling 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki cfa4b9a291 added current state to device details 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 9a9970336b added view and url for new action 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 2f23ed7e88 state institution now nullable 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 7470dc5de2 condition checking and renaming 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 57954d66e2 stylish new popup for state change 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki ede5d6a6c5 added Sortable js dependency for tables 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 62ccf46194 added form for state definition order update 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 3b0735faec minor cosmetic changes 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 3670fda2a3 changed delete button to icon 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 6d44aa855b send button hidden until changes are made 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 4485517221 sortable list now updates order of definitions 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki b9ffd788a1 added Sortable js for state definitions list 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki bad82965e9 added model level constraints 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki eecaef7cff modals for state definition deletion 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki fb6c243ee8 delete view added and some refactorig 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki ff9a78ed23 added add state definition view 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 5fcd9ce7ca changed constrain on model 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 539f5b5bb7 fixed models confusion 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 5e2c8f2328 added model constraints 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 2451e843ac added admin state definition panel 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki acbe2f6a75 initial orm state models 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 2bb9e8d035 centered popup modals 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 35622ff9c7 disabled lotPoperty field 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki ff928a381b added logging for device operations 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 09bed0a904 renaming lotAnnotations to Lotproperty 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 9807cb56aa renaming of annotation to property 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki e0e4fd862a change edit view to modal popup 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki b4f0909199 added userproperty update view 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki db59b099f5 added user_property 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 963858263a model constraints changed and moved url to /device 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 4bc423a979 fixed search and moved delete user property class 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 4d9f588ad7 renaming to new Property tables 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 42b13eec84 details view changed to now use properties 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki fcefddb5a0 fixed user_properties list not working 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 8991faa423 renaming to property 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 9df611293a changed imports 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 9c27a30399 fixed self inflicted recursion 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 13d325105f renaming annotation to variable 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki 09c3f96185 variables and function semantic renaming 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki dad8e40ee8 split into system and user properties 2025-01-31 16:47:42 +01:00
Thomas Nahuel Rusiecki f7cd7bc3f2 annotations renaming on views 2025-01-31 16:47:42 +01:00
pedro 1f0a9a60ce add file snapshot_pre-verifiable-credential 2025-01-30 13:26:47 +01:00
pedro 01b7267dd8 docker: add DEMO env var to idhub 2025-01-30 13:11:55 +01:00
pedro 91c03cb990 Merge pull request 'add did document link to device details page' (#44) from diddocument into main
Reviewed-on: #44
2025-01-30 08:50:38 +00:00
pedro 9dafc51210 disable devicehub demo by default (part 2) 2025-01-30 09:27:55 +01:00
pedro 7f16552762 disable devicehub demo by default
(right now is not working, we are waiting open service feature)
2025-01-30 09:19:37 +01:00
pedro 829fb6e2a1 devicehub: don't rush on the vc signing 2025-01-30 09:05:06 +01:00
pedro 02a69e6994 devicehub: add error handling when waiting idhub 2025-01-30 08:59:01 +01:00
pedro 2ff630f212 docker: attempt to fix docker image deployment 2025-01-30 08:46:27 +01:00
pedro d7d6fb7bc6 make devicehub autosign a vc using idhub 2025-01-30 08:39:22 +01:00
pedro 096704935d better error message on receiving snapshot 2025-01-30 08:09:20 +01:00
pedro 0485604512 rename credentials according to last meeting
we found a way with cayo to generate a real credential on demo time,
and that's going to be done on following commits in this branch

related ereuse/projectes#131
2025-01-29 20:20:23 +01:00
pedro 40b0617a72 docker: add the missing predefined token config 2025-01-29 19:25:26 +01:00
pedro b1c4a2cec9 add the right SUPPORTED_CREDENTIALS 2025-01-29 19:19:57 +01:00
Cayo Puigdefabregas 9503a9a8b4 add did document to details page 2025-01-29 19:07:11 +01:00
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
pedro 78de86c43a Merge pull request 'inxi (take 2)' (#38) from inxi into main
Reviewed-on: #38
2024-12-16 18:48:41 +00:00
pedro 498da24ee6 docker-reset: bugfix to reset, add flag file 2024-12-16 19:37:59 +01:00
pedro 67213e46d2 bugfix typo on entrypoint 2024-12-16 19:29:48 +01:00
pedro 5ec44d3378 better .env.example (just edit DOMAIN) 2024-12-16 19:28:08 +01:00
pedro 1d195806aa bad bugfix: docker entrypoint overrides .env
because of dpp
2024-12-16 19:25:32 +01:00
pedro d8202fea0f dockerfile: remove unnecessary pip upgrade 2024-12-16 19:19:19 +01:00
pedro e6fea8c1c3 update requirements so works on lxc-docker 2024-12-16 19:19:16 +01:00
Cayo Puigdefabregas 782f6dac51 refactor inxi 2024-12-16 18:12:02 +01:00
Cayo Puigdefabregas 46bbb940d7 fix parsing with credentials 2024-12-16 18:04:07 +01:00
Cayo Puigdefabregas abdefe885e fix parsing 2024-12-16 18:04:07 +01:00
Cayo Puigdefabregas 1902647337 fix get_hid 2024-12-16 18:04:07 +01:00
Cayo Puigdefabregas 989120bd0a fix component empty 2024-12-16 18:04:07 +01:00
Cayo Puigdefabregas dbdcffe5d9 add inxi in parsing and show in details of devs 2024-12-16 18:04:03 +01:00
pedro 1ea400051c Merge pull request 'DPP/DLT functionality' (#36) from dpp into main
Reviewed-on: #36
2024-12-11 15:15:14 +00:00
pedro f5ea0f8d8d docker entrypoint: bugfix when DPP env var unbound 2024-12-11 11:25:35 +01:00
pedro c5dd3a759d docker entrypoint: adapt it to DPP env var 2024-12-11 11:23:35 +01:00
pedro 3bf23c5d3b propagate DPP env var to docker 2024-12-11 11:16:32 +01:00
Cayo Puigdefabregas e54d8ba49c activate/deactivate DPP from env 2024-12-05 10:54:41 +01:00
Cayo Puigdefabregas 7f44d88b61 convert jsonld in credentials for dpps 2024-12-04 18:25:16 +01:00
Cayo Puigdefabregas b2e6406fb6 drop loggers 2024-11-29 17:23:44 +01:00
Cayo Puigdefabregas b477431fcd fix register dpp 2024-11-29 16:00:10 +01:00
Cayo Puigdefabregas 2c21977c7c fix did dpp 2024-11-29 15:48:48 +01:00
Cayo Puigdefabregas 563e394afc debug timestamp 2024-11-28 21:24:59 +01:00
pedro f566bfdf03 bugfix attempt verifyProof
co-authored with cayo
2024-11-28 21:14:32 +01:00
Cayo Puigdefabregas f04d425a31 more and more debug 2024-11-28 20:59:43 +01:00
Cayo Puigdefabregas 8aefc90ece more debug 2024-11-28 20:21:21 +01:00
Cayo Puigdefabregas 13249d564f fix call to proofs 2024-11-28 18:48:50 +01:00
Cayo Puigdefabregas ddb764b3ea dpp for proofs 2024-11-28 17:25:40 +01:00
Cayo Puigdefabregas 351c62b45d dpp for proofs 2024-11-28 17:22:42 +01:00
Cayo Puigdefabregas 16a3a870be drop actions for dpp 2024-11-28 17:13:21 +01:00
Cayo Puigdefabregas 76c4b10fc4 drop actions for dpp 2024-11-28 17:01:44 +01:00
Cayo Puigdefabregas 1910609f68 debug in proof call 2024-11-28 16:54:31 +01:00
Cayo Puigdefabregas 53524643c8 fix cors origin 2024-11-28 16:37:38 +01:00
Cayo Puigdefabregas 9b7bbd6bcf fix json call to chid 2024-11-28 16:22:06 +01:00
Cayo Puigdefabregas f7f6da5892 fix phid hash list 2024-11-28 15:47:03 +01:00
Cayo Puigdefabregas 8643495a9d fix phid hash list 2024-11-28 15:40:09 +01:00
Cayo Puigdefabregas dd327e5231 fix phid 2024-11-28 15:33:57 +01:00
Cayo Puigdefabregas cea5a279b7 fix 2024-11-28 15:25:57 +01:00
Cayo Puigdefabregas 594904905b new document and out device and components 2024-11-28 12:21:34 +01:00
pedro 74f0d5a507 comment logger trace when DEBUG
is it necessary?
2024-11-28 00:00:09 +01:00
Cayo Puigdefabregas 6d53a2acda remove flask sintax for django sintax 2024-11-27 18:41:17 +01:00
Cayo Puigdefabregas e64e9b3c06 remove flask sintax for django sintax 2024-11-27 18:36:08 +01:00
Cayo Puigdefabregas f1c21af654 add_services 2024-11-27 18:29:36 +01:00
Cayo Puigdefabregas 6efcf5ac18 add result for dpp and for chid 2024-11-27 18:11:22 +01:00
Cayo Puigdefabregas 5631da453a fix new document json 2024-11-27 16:53:31 +01:00
Cayo Puigdefabregas 0c5adf87c6 fix get_result 2024-11-27 15:59:23 +01:00
pedro dde4a114a0 dhub settings: bugfix wrong DLT TOKEN 2024-11-27 15:50:33 +01:00
Cayo Puigdefabregas e791d0c63c fix get_result for get correct document 2024-11-27 15:50:07 +01:00
pedro 833c458840 progress on making it work
still fails
2024-11-27 15:06:41 +01:00
Cayo Puigdefabregas e6424af251 remove pdb 2024-11-27 13:08:43 +01:00
Cayo Puigdefabregas d31c5c7921 get_result for json 2024-11-27 13:07:13 +01:00
Cayo Puigdefabregas 5432d22f35 view dpp page 2024-11-27 13:07:13 +01:00
Cayo Puigdefabregas fdc431b43a fix 2024-11-27 13:07:13 +01:00
pedro ba6c955463 no sudo in docker-reset, all is with user 1000 2024-11-27 09:31:13 +01:00
pedro 8eb89e602d dh-django dockerfile: use uid 1000 (app)
at least temporarily
2024-11-27 09:28:39 +01:00
pedro 80ab639759 dh docker: bugfix wrong usage of up_snapshots 2024-11-27 01:38:24 +01:00
pedro 162fc9a8ef dh dockerfile: add time debpkg 2024-11-27 01:31:15 +01:00
pedro eec14f4ffb dh docker: bugfix wrong path in rm prev snapshots 2024-11-27 01:28:19 +01:00
pedro 3ab21b8c32 dh docker: create institution before first dlt usr 2024-11-27 01:25:15 +01:00
pedro 316184df47 bugfix logger 2024-11-27 01:21:52 +01:00
pedro 7d6119c11a logger: bugfix function name changed for highlight 2024-11-27 01:12:58 +01:00
pedro 1248ab6894 dh docker: cleanup other snapshots when dpp/dlt 2024-11-27 01:10:34 +01:00
pedro 41e4374612 add dpp 2024-11-27 01:09:06 +01:00
pedro 6e7cb0bf84 dh docker: first migrate, then config 2024-11-27 01:08:22 +01:00
pedro 09f6cd2e68 utils/logger: ensure msgs are logged 2024-11-27 00:52:20 +01:00
pedro e25b8e5994 logger: improve error handling 2024-11-27 00:38:03 +01:00
pedro 2793344684 dpp/dlt: fix typo 2024-11-27 00:21:41 +01:00
pedro f70c75a92f dpp/dlt: fix typo 2024-11-27 00:17:41 +01:00
pedro 83719edc8e dpp/dlt: fix typo 2024-11-27 00:15:16 +01:00
pedro 69bc95c0cd dh docker entrypoint: use appropriate new env vars 2024-11-27 00:11:44 +01:00
pedro 9863a59911 docker: remove unused vars in django
were used in the flask app devicehub-teal
2024-11-26 19:25:30 +01:00
pedro eb78d38c3a docker entrypoint: make DB_* optional 2024-11-26 12:47:15 +01:00
pedro a49b31dd85 docker devicehub-django entrypoint 2024-11-22 12:07:25 +01:00
Cayo Puigdefabregas 493c7636b2 fix 2024-11-20 12:04:56 +01:00
Cayo Puigdefabregas 88e036eb3c fix 2024-11-20 12:03:20 +01:00
Cayo Puigdefabregas b759c53e75 add memberFederated model 2024-11-20 10:52:22 +01:00
Cayo Puigdefabregas 7ab88ad290 add did view 2024-11-19 21:18:45 +01:00
Cayo Puigdefabregas f6d1cf719c add commands for setup to dlt 2024-11-19 21:18:45 +01:00
pedro fb836edfbb docker: add dpp python dep ereuseapitest 2024-11-19 18:11:48 +01:00
Cayo Puigdefabregas cc350775ed . 2024-11-19 18:02:43 +01:00
Cayo Puigdefabregas 0d574cae63 register device and dpp in dlt and dpp api 2024-11-18 19:37:08 +01:00
Cayo Puigdefabregas 9857891b63 first base for dpp 2024-11-15 18:56:11 +01:00
129 changed files with 5776 additions and 1673 deletions

3
.dockerignore Normal file
View file

@ -0,0 +1,3 @@
db
.git
already_configured

17
.editorconfig Normal file
View file

@ -0,0 +1,17 @@
# repository editor configuration normalization
# @see http://editorconfig.org/
# This is the top-most .editorconfig file; do not search in parent directories.
root = true
# All files.
[*]
end_of_line = LF
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.sh]
indent_size = 8

View file

@ -1,8 +1,19 @@
DOMAIN=localhost ####
# DEV OPTIONS
####
DEV_DOCKER_ALWAYS_BUILD=false
####
# DEVICEHUB
####
DEVICEHUB_DOMAIN=localhost
DEVICEHUB_PORT=8001
DEMO=true DEMO=true
# note that with DEBUG=true, logs are more verbose (include tracebacks) # note that with DEBUG=true, logs are more verbose (include tracebacks)
DEBUG=true DEBUG=true
ALLOWED_HOSTS=localhost,localhost:8000,127.0.0.1, DPP=false
STATIC_ROOT=/tmp/static/ STATIC_ROOT=/tmp/static/
MEDIA_ROOT=/tmp/media/ MEDIA_ROOT=/tmp/media/
@ -15,6 +26,48 @@ EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
EMAIL_FILE_PATH="/tmp/app-messages" EMAIL_FILE_PATH="/tmp/app-messages"
ENABLE_EMAIL=false ENABLE_EMAIL=false
PREDEFINED_TOKEN='5018dd65-9abd-4a62-8896-80f34ac66150' 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 # TODO review these vars
#SNAPSHOTS_DIR=/path/to/TODO #SNAPSHOTS_DIR=/path/to/TODO
#EVIDENCES_DIR=/path/to/TODO #EVIDENCES_DIR=/path/to/TODO
#DEMO_IDHUB_DOMAIN='idhub.example.org'
####
# 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
IDHUB_PREDEFINED_TOKEN='27f944ce-3d58-4f48-b068-e4aa95f97c95'

5
.gitignore vendored
View file

@ -1,4 +1,7 @@
db.sqlite3
env/ env/
__pycache__/ __pycache__/
.env .env
# the following could be autogenerated by devicehub
db.sqlite3
example/snapshots/snapshot_workbench-script_verifiable-credential.json

16
action/forms.py Normal file
View file

@ -0,0 +1,16 @@
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

@ -0,0 +1,43 @@
#!/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

@ -0,0 +1,83 @@
# 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

@ -0,0 +1,89 @@
# 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,3 +1,83 @@
from django.db import models from django.db import models, connection
from django.db.models import Max
from user.models import User, Institution
from django.core.exceptions import ValidationError
# Create your models here. 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}"

View file

@ -1 +1,12 @@
from django.urls import path, include 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 +1,141 @@
# from django.shortcuts import render 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.contrib.auth.mixins import LoginRequiredMixin
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(LoginRequiredMixin, 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(LoginRequiredMixin, 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(LoginRequiredMixin, 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(LoginRequiredMixin, 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)

5
admin/forms.py Normal file
View file

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

View file

@ -0,0 +1,233 @@
{% 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,4 +10,9 @@ urlpatterns = [
path("users/edit/<int:pk>", views.EditUserView.as_view(), name="edit_user"), 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("users/delete/<int:pk>", views.DeleteUserView.as_view(), name="delete_user"),
path("institution/<int:pk>", views.InstitutionView.as_view(), name="institution"), 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,16 +1,23 @@
import logging
from smtplib import SMTPException from smtplib import SMTPException
from django.contrib import messages
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404, redirect, Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic.base import TemplateView from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic.base import TemplateView, ContextMixin
from django.views.generic.edit import ( from django.views.generic.edit import (
CreateView, CreateView,
UpdateView, UpdateView,
DeleteView, DeleteView,
) )
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from dashboard.mixins import DashboardView, Http403 from dashboard.mixins import DashboardView, Http403
from admin.forms import OrderingStateForm
from user.models import User, Institution from user.models import User, Institution
from admin.email import NotifyActivateUserByEmail from admin.email import NotifyActivateUserByEmail
from action.models import StateDefinition
class AdminView(DashboardView): class AdminView(DashboardView):
@ -18,7 +25,7 @@ class AdminView(DashboardView):
response = super().get(*args, **kwargs) response = super().get(*args, **kwargs)
if not self.request.user.is_admin: if not self.request.user.is_admin:
raise Http403 raise Http403
return response return response
class PanelView(AdminView, TemplateView): class PanelView(AdminView, TemplateView):
@ -104,7 +111,7 @@ class EditUserView(AdminView, UpdateView):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
return kwargs return kwargs
class InstitutionView(AdminView, UpdateView): class InstitutionView(AdminView, UpdateView):
template_name = "institution.html" template_name = "institution.html"
title = _("Edit institution") title = _("Edit institution")
@ -124,3 +131,109 @@ class InstitutionView(AdminView, UpdateView):
self.object = self.request.user.institution self.object = self.request.user.institution
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
return 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):
super().form_invalid(form)
return redirect(self.success_url)
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']
success_url = reverse_lazy('admin:states_panel')
pk_url_kwarg = 'pk'
def get_queryset(self):
return StateDefinition.objects.filter(institution=self.request.user.institution)
def form_valid(self, form):
try:
response = super().form_valid(form)
messages.success(self.request, _("State definition updated successfully."))
return response
except IntegrityError:
messages.error(self.request, _("State is already defined."))
return self.form_invalid(form)
def form_invalid(self, form):
super().form_invalid(form)
return redirect(self.get_success_url())

View file

@ -7,7 +7,7 @@ app_name = 'api'
urlpatterns = [ urlpatterns = [
path('v1/snapshot/', views.NewSnapshotView.as_view(), name='new_snapshot'), path('v1/snapshot/', views.NewSnapshotView.as_view(), name='new_snapshot'),
path('v1/annotation/<str:pk>/', views.AddAnnotationView.as_view(), name='new_annotation'), path('v1/property/<str:pk>/', views.AddPropertyView.as_view(), name='new_property'),
path('v1/device/<str:pk>/', views.DetailsDeviceView.as_view(), name='device'), path('v1/device/<str:pk>/', views.DetailsDeviceView.as_view(), name='device'),
path('v1/tokens/', views.TokenView.as_view(), name='tokens'), path('v1/tokens/', views.TokenView.as_view(), name='tokens'),
path('v1/tokens/new', views.TokenNewView.as_view(), name='new_token'), 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 utils.save_snapshots import move_json, save_in_disk
from django.views.generic.edit import View 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 SystemProperty, UserProperty
from evidence.parse_details import ParseSnapshot from evidence.parse_details import ParseSnapshot
from evidence.parse import Build from evidence.parse import Build
from device.models import Device from device.models import Device
@ -85,17 +85,21 @@ class NewSnapshotView(ApiMixing):
# except Exception: # except Exception:
# return JsonResponse({'error': 'Invalid Snapshot'}, status=400) # return JsonResponse({'error': 'Invalid Snapshot'}, status=400)
if not data.get("uuid"): ev_uuid = data.get("uuid")
txt = "error: the snapshot not have uuid" if data.get("credentialSubject"):
ev_uuid = data["credentialSubject"].get("uuid")
if not ev_uuid:
txt = "error: the snapshot does not have an uuid"
logger.error("%s", txt) logger.error("%s", txt)
return JsonResponse({'status': txt}, status=500) return JsonResponse({'status': txt}, status=500)
exist_annotation = Annotation.objects.filter( exist_property = SystemProperty.objects.filter(
uuid=data['uuid'] uuid=ev_uuid
).first() ).first()
if exist_annotation: if exist_property:
txt = "error: the snapshot {} exist".format(data['uuid']) txt = "error: the snapshot {} exist".format(ev_uuid)
logger.warning("%s", txt) logger.warning("%s", txt)
return JsonResponse({'status': txt}, status=500) return JsonResponse({'status': txt}, status=500)
@ -105,31 +109,30 @@ class NewSnapshotView(ApiMixing):
except Exception as err: except Exception as err:
if settings.DEBUG: if settings.DEBUG:
logger.exception("%s", err) logger.exception("%s", err)
snapshot_id = data.get("uuid", "") snapshot_id = ev_uuid
txt = "It is not possible to parse snapshot: %s." txt = "It is not possible to parse snapshot: %s."
logger.error(txt, snapshot_id) logger.error(txt, snapshot_id)
text = "fail: It is not possible to parse snapshot" text = "fail: It is not possible to parse snapshot"
return JsonResponse({'status': text}, status=500) return JsonResponse({'status': text}, status=500)
annotation = Annotation.objects.filter( prop = SystemProperty.objects.filter(
uuid=data['uuid'], uuid=ev_uuid,
type=Annotation.Type.SYSTEM,
# TODO this is hardcoded, it should select the user preferred algorithm # TODO this is hardcoded, it should select the user preferred algorithm
key="hidalgo1", key="ereuse24",
owner=self.tk.owner.institution owner=self.tk.owner.institution
).first() ).first()
if not annotation: if not prop:
logger.error("Error: No annotation for uuid: %s", data["uuid"]) logger.error("Error: No property for uuid: %s", ev_uuid)
return JsonResponse({'status': 'fail'}, status=500) return JsonResponse({'status': 'fail'}, status=500)
url_args = reverse_lazy("device:details", args=(annotation.value,)) url_args = reverse_lazy("device:details", args=(property.value,))
url = request.build_absolute_uri(url_args) url = request.build_absolute_uri(url_args)
response = { response = {
"status": "success", "status": "success",
"dhid": annotation.value[:6].upper(), "dhid": property.value[:6].upper(),
"url": url, "url": url,
# TODO replace with public_url when available # TODO replace with public_url when available
"public_url": url "public_url": url
@ -255,22 +258,21 @@ class DetailsDeviceView(ApiMixing):
"components": snapshot.get("components"), "components": snapshot.get("components"),
}) })
uuids = Annotation.objects.filter( uuids = SystemProperty.objects.filter(
owner=self.tk.owner.institution, owner=self.tk.owner.institution,
value=self.pk value=self.pk
).values("uuid") ).values("uuid")
annotations = Annotation.objects.filter( properties = UserProperty.objects.filter(
uuid__in=uuids, uuid__in=uuids,
owner=self.tk.owner.institution, owner=self.tk.owner.institution,
type = Annotation.Type.USER
).values_list("key", "value") ).values_list("key", "value")
data.update({"annotations": list(annotations)}) data.update({"properties": list(properties)})
return data return data
class AddAnnotationView(ApiMixing): class AddPropertyView(ApiMixing):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
response = self.auth() response = self.auth()
@ -279,13 +281,12 @@ class AddAnnotationView(ApiMixing):
self.pk = kwargs['pk'] self.pk = kwargs['pk']
institution = self.tk.owner.institution institution = self.tk.owner.institution
self.annotation = Annotation.objects.filter( self.property = SystemProperty.objects.filter(
owner=institution, owner=institution,
value=self.pk, value=self.pk,
type=Annotation.Type.SYSTEM
).first() ).first()
if not self.annotation: if not self.property:
return JsonResponse({}, status=404) return JsonResponse({}, status=404)
try: try:
@ -296,10 +297,9 @@ class AddAnnotationView(ApiMixing):
logger.error("Invalid Snapshot of user %s", self.tk.owner) logger.error("Invalid Snapshot of user %s", self.tk.owner)
return JsonResponse({'error': 'Invalid JSON'}, status=500) return JsonResponse({'error': 'Invalid JSON'}, status=500)
Annotation.objects.create( UserProperty.objects.create(
uuid=self.annotation.uuid, uuid=self.property.uuid,
owner=self.tk.owner.institution, owner=self.tk.owner.institution,
type = Annotation.Type.USER,
key = key, key = key,
value = value value = value
) )

View file

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

File diff suppressed because one or more lines are too long

2
dashboard/static/js/Sortable.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -15,9 +15,10 @@
<!-- Bootstrap core CSS --> <!-- Bootstrap core CSS -->
{% block style %} {% block style %}
<script src="{% static 'fontawesomefree/js/all.min.js' %}"></script> <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"> <link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet">
<link href="{% static "/css/bootstrap-icons.min.css" %}" rel="stylesheet"> <script src="{% static 'js/Sortable.min.js' %}"></script>
<style> <style>
.bd-placeholder-img { .bd-placeholder-img {
@ -51,7 +52,7 @@
} }
</style> </style>
<!-- Custom styles for this template --> <!-- Custom styles for this template -->
<link href="{% static "/css/dashboard.css" %}" rel="stylesheet"> <link href="{% static "/css/dashboard.css" %}" rel="stylesheet">
{% endblock %} {% endblock %}
@ -81,21 +82,26 @@
<ul class="nav flex-column"> <ul class="nav flex-column">
{% if user.is_admin %} {% if user.is_admin %}
<li class="nav-item"> <li class="nav-item">
<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()"> <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()">
<i class="bi bi-person-fill-gear icon_sidebar"></i> <i class="bi bi-person-fill-gear icon_sidebar"></i>
{% trans 'Admin' %} {% trans 'Admin' %}
</a> </a>
<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"> <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">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if path == 'panel' %} active2{% endif %}" href="{% url 'admin:panel' %}"> <a class="nav-link{% if path in 'panel institution' %} active2{% endif %}" href="{% url 'admin:panel' %}">
{% trans 'Panel' %} {% trans 'Panel' %}
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if path == 'users' %} active2{% endif %}" href="{% url 'admin:users' %}"> <a class="nav-link{% if path in 'users edit_user new_user delete_user' %} active2{% endif %}" href="{% url 'admin:users' %}">
{% trans 'Users' %} {% trans 'Users' %}
</a> </a>
</li> </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> </ul>
</li> </li>
{% endif %} {% endif %}
@ -178,9 +184,15 @@
{% endfor %} {% endfor %}
{% endblock messages %} {% endblock messages %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2"> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2">
<h1 class="h2">{{ title }}</h1> <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>
<form method="post" action="{% url 'dashboard:search' %}"> <form method="post" action="{% url 'dashboard:search' %}">
{% csrf_token %} {% csrf_token %}
<div class="input-group rounded"> <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" /> <input type="search" name="search" class="form-control rounded" placeholder="Search your device..." aria-label="Search" aria-describedby="search-addon" />
@ -189,9 +201,9 @@
</span> </span>
</div> </div>
</form> </form>
</div> </div>
<div class="row border-bottom mb-3"> <div class="row border-bottom mb-3">
<div class="col"> <div class="col">
<small style="color:#899bbd"><i>{{ breadcrumb }}</i></small> <small style="color:#899bbd"><i>{{ breadcrumb }}</i></small>
@ -221,4 +233,13 @@
{% block extrascript %}{% endblock %} {% block extrascript %}{% endblock %}
{% endblock %} {% endblock %}
</body> </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> </html>

View file

@ -9,20 +9,14 @@
<h3>{{ subtitle }}</h3> <h3>{{ subtitle }}</h3>
</div> </div>
<div class="col text-center"> <div class="col text-center">
{% if lot %}
<a href="{% url 'lot:documents' object.id %}" type="button" class="btn btn-green-admin">
<i class="bi bi-folder2"></i>
{% trans 'Documents' %}
</a>
{% endif %}
<a href="{# url 'dashboard:exports' object.id #}" type="button" class="btn btn-green-admin"> <a href="{# url 'dashboard:exports' object.id #}" type="button" class="btn btn-green-admin">
<i class="bi bi-reply"></i> <i class="bi bi-reply"></i>
{% trans 'Exports' %} {% trans 'Exports' %}
</a> </a>
{% if lot %} {% if lot %}
<a href="{% url 'lot:annotations' object.id %}" type="button" class="btn btn-green-admin"> <a href="{% url 'lot:properties' object.id %}" type="button" class="btn btn-green-admin">
<i class="bi bi-tag"></i> <i class="bi bi-tag"></i>
{% trans 'Annotations' %} {% trans 'properties' %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
@ -69,7 +63,11 @@
{{ dev.manufacturer }} {{ dev.manufacturer }}
</td> </td>
<td> <td>
{% if dev.version %}
{{dev.version}} {{ dev.model }}
{% else %}
{{ dev.model }} {{ dev.model }}
{% endif %}
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

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

View file

@ -1,5 +1,5 @@
from django import forms from django import forms
from utils.device import create_annotation, create_doc, create_index from utils.device import create_property, create_doc, create_index
from utils.save_snapshots import move_json, save_in_disk 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") path_name = save_in_disk(doc, self.user.institution.name, place="placeholder")
create_index(doc, self.user) create_index(doc, self.user)
create_annotation(doc, user, commit=commit) create_property(doc, user, commit=commit)
move_json(path_name, self.user.institution.name, place="placeholder") move_json(path_name, self.user.institution.name, place="placeholder")
return doc return doc

View file

@ -1,8 +1,9 @@
from django.db import models, connection from django.db import models, connection
from utils.constants import ALGOS from utils.constants import ALGOS
from evidence.models import Annotation, Evidence from evidence.models import SystemProperty, UserProperty, Evidence
from lot.models import DeviceLot from lot.models import DeviceLot
from action.models import State
class Device: class Device:
@ -25,11 +26,12 @@ class Device:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# the id is the chid of the device # the id is the chid of the device
self.id = kwargs["id"] self.id = kwargs["id"]
self.uuid = kwargs.get("uuid")
self.pk = self.id self.pk = self.id
self.shortid = self.pk[:6].upper() self.shortid = self.pk[:6].upper()
self.algorithm = None self.algorithm = None
self.owner = None self.owner = None
self.annotations = [] self.properties = []
self.hids = [] self.hids = []
self.uuids = [] self.uuids = []
self.evidences = [] self.evidences = []
@ -38,61 +40,48 @@ class Device:
self.get_last_evidence() self.get_last_evidence()
def initial(self): def initial(self):
self.get_annotations() self.get_properties()
self.get_uuids() self.get_uuids()
self.get_hids() self.get_hids()
self.get_evidences() self.get_evidences()
self.get_lots() self.get_lots()
def get_annotations(self): def get_properties(self):
if self.annotations: if self.properties:
return self.annotations return self.properties
self.annotations = Annotation.objects.filter( self.properties = SystemProperty.objects.filter(
type=Annotation.Type.SYSTEM,
value=self.id value=self.id
).order_by("-created") ).order_by("-created")
if self.annotations.count(): if self.properties.count():
self.algorithm = self.annotations[0].key self.algorithm = self.properties[0].key
self.owner = self.annotations[0].owner self.owner = self.properties[0].owner
return self.annotations return self.properties
def get_user_annotations(self): def get_user_properties(self):
if not self.uuids: if not self.uuids:
self.get_uuids() self.get_uuids()
annotations = Annotation.objects.filter( user_properties = UserProperty.objects.filter(
uuid__in=self.uuids, uuid__in=self.uuids,
owner=self.owner, owner=self.owner,
type=Annotation.Type.USER type=UserProperty.Type.USER,
) )
return annotations return user_properties
def get_user_documents(self):
if not self.uuids:
self.get_uuids()
annotations = Annotation.objects.filter(
uuid__in=self.uuids,
owner=self.owner,
type=Annotation.Type.DOCUMENT
)
return annotations
def get_uuids(self): def get_uuids(self):
for a in self.get_annotations(): for a in self.get_properties():
if a.uuid not in self.uuids: if a.uuid not in self.uuids:
self.uuids.append(a.uuid) self.uuids.append(a.uuid)
def get_hids(self): def get_hids(self):
annotations = self.get_annotations() properties = self.get_properties()
algos = list(ALGOS.keys()) algos = list(ALGOS.keys())
algos.append('CUSTOM_ID') algos.append('CUSTOM_ID')
self.hids = list(set(annotations.filter( self.hids = list(set(properties.filter(
type=Annotation.Type.SYSTEM,
key__in=algos, key__in=algos,
).values_list("value", flat=True))) ).values_list("value", flat=True)))
@ -103,11 +92,19 @@ class Device:
self.evidences = [Evidence(u) for u in self.uuids] self.evidences = [Evidence(u) for u in self.uuids]
def get_last_evidence(self): def get_last_evidence(self):
annotations = self.get_annotations() if self.last_evidence:
if not annotations.count():
return return
annotation = annotations.first()
self.last_evidence = Evidence(annotation.uuid) if self.uuid:
self.last_evidence = Evidence(self.uuid)
return
properties = self.get_properties()
if not properties.count():
return
prop = properties.first()
self.last_evidence = Evidence(prop.uuid)
def is_eraseserver(self): def is_eraseserver(self):
if not self.uuids: if not self.uuids:
@ -115,19 +112,26 @@ class Device:
if not self.uuids: if not self.uuids:
return False return False
annotation = Annotation.objects.filter( prop = UserProperty.objects.filter(
uuid__in=self.uuids, uuid__in=self.uuids,
owner=self.owner, owner=self.owner,
type=Annotation.Type.ERASE_SERVER type=UserProperty.Type.ERASE_SERVER
).first() ).first()
if annotation: if prop:
return True return True
return False return False
def last_uuid(self): def last_uuid(self):
if self.uuid:
return self.uuid
return self.uuids[0] 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): def get_lots(self):
self.lots = [ self.lots = [
x.lot for x in DeviceLot.objects.filter(device_id=self.id)] x.lot for x in DeviceLot.objects.filter(device_id=self.id)]
@ -136,7 +140,7 @@ class Device:
def get_unassigned(cls, institution, offset=0, limit=None): def get_unassigned(cls, institution, offset=0, limit=None):
sql = """ sql = """
WITH RankedAnnotations AS ( WITH RankedProperties AS (
SELECT SELECT
t1.value, t1.value,
t1.key, t1.key,
@ -145,38 +149,36 @@ class Device:
ORDER BY ORDER BY
CASE CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1 WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = 'hidalgo1' THEN 2 WHEN t1.key = 'ereuse24' THEN 2
ELSE 3 ELSE 3
END, END,
t1.created DESC t1.created DESC
) AS row_num ) AS row_num
FROM evidence_annotation AS t1 FROM evidence_systemproperty AS t1
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
WHERE t2.device_id IS NULL WHERE t2.device_id IS NULL
AND t1.owner_id = {institution} AND t1.owner_id = {institution}
AND t1.type = {type}
) )
SELECT DISTINCT SELECT DISTINCT
value value
FROM FROM
RankedAnnotations RankedProperties
WHERE WHERE
row_num = 1 row_num = 1
""".format( """.format(
institution=institution.id, institution=institution.id,
type=Annotation.Type.SYSTEM,
) )
if limit: if limit:
sql += " limit {} offset {}".format(int(limit), int(offset)) sql += " limit {} offset {}".format(int(limit), int(offset))
sql += ";" sql += ";"
annotations = [] properties = []
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute(sql) cursor.execute(sql)
annotations = cursor.fetchall() properties = cursor.fetchall()
devices = [cls(id=x[0]) for x in annotations] devices = [cls(id=x[0]) for x in properties]
count = cls.get_unassigned_count(institution) count = cls.get_unassigned_count(institution)
return devices, count return devices, count
@ -184,7 +186,7 @@ class Device:
def get_unassigned_count(cls, institution): def get_unassigned_count(cls, institution):
sql = """ sql = """
WITH RankedAnnotations AS ( WITH RankedProperties AS (
SELECT SELECT
t1.value, t1.value,
t1.key, t1.key,
@ -193,35 +195,33 @@ class Device:
ORDER BY ORDER BY
CASE CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1 WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = 'hidalgo1' THEN 2 WHEN t1.key = 'ereuse24' THEN 2
ELSE 3 ELSE 3
END, END,
t1.created DESC t1.created DESC
) AS row_num ) AS row_num
FROM evidence_annotation AS t1 FROM evidence_systemproperty AS t1
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
WHERE t2.device_id IS NULL WHERE t2.device_id IS NULL
AND t1.owner_id = {institution} AND t1.owner_id = {institution}
AND t1.type = {type}
) )
SELECT SELECT
COUNT(DISTINCT value) COUNT(DISTINCT value)
FROM FROM
RankedAnnotations RankedProperties
WHERE WHERE
row_num = 1 row_num = 1
""".format( """.format(
institution=institution.id, institution=institution.id,
type=Annotation.Type.SYSTEM,
) )
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute(sql) cursor.execute(sql)
return cursor.fetchall()[0][0] return cursor.fetchall()[0][0]
@classmethod @classmethod
def get_annotation_from_uuid(cls, uuid, institution): def get_properties_from_uuid(cls, uuid, institution):
sql = """ sql = """
WITH RankedAnnotations AS ( WITH RankedProperties AS (
SELECT SELECT
t1.value, t1.value,
t1.key, t1.key,
@ -230,78 +230,79 @@ class Device:
ORDER BY ORDER BY
CASE CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1 WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = 'hidalgo1' THEN 2 WHEN t1.key = 'ereuse24' THEN 2
ELSE 3 ELSE 3
END, END,
t1.created DESC t1.created DESC
) AS row_num ) AS row_num
FROM evidence_annotation AS t1 FROM evidence_systemproperty AS t1
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
WHERE t2.device_id IS NULL WHERE t2.device_id IS NULL
AND t1.owner_id = {institution} AND t1.owner_id = {institution}
AND t1.type = {type}
AND t1.uuid = '{uuid}' AND t1.uuid = '{uuid}'
) )
SELECT DISTINCT SELECT DISTINCT
value value
FROM FROM
RankedAnnotations RankedProperties
WHERE WHERE
row_num = 1; row_num = 1;
""".format( """.format(
uuid=uuid.replace("-", ""), uuid=uuid.replace("-", ""),
institution=institution.id, institution=institution.id,
type=Annotation.Type.SYSTEM,
) )
annotations = [] properties = []
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute(sql) cursor.execute(sql)
annotations = cursor.fetchall() properties = cursor.fetchall()
return cls(id=annotations[0][0]) return cls(id=properties[0][0])
@property @property
def is_websnapshot(self): def is_websnapshot(self):
if not self.last_evidence: self.get_last_evidence()
self.get_last_evidence()
return self.last_evidence.doc['type'] == "WebSnapshot" return self.last_evidence.doc['type'] == "WebSnapshot"
@property @property
def last_user_evidence(self): def last_user_evidence(self):
if not self.last_evidence: self.get_last_evidence()
self.get_last_evidence()
return self.last_evidence.doc['kv'].items() return self.last_evidence.doc['kv'].items()
@property @property
def manufacturer(self): def manufacturer(self):
if not self.last_evidence: self.get_last_evidence()
self.get_last_evidence()
return self.last_evidence.get_manufacturer() return self.last_evidence.get_manufacturer()
@property @property
def serial_number(self): def serial_number(self):
if not self.last_evidence: self.get_last_evidence()
self.get_last_evidence()
return self.last_evidence.get_serial_number() return self.last_evidence.get_serial_number()
@property @property
def type(self): def type(self):
self.get_last_evidence()
if self.last_evidence.doc['type'] == "WebSnapshot": if self.last_evidence.doc['type'] == "WebSnapshot":
return self.last_evidence.doc.get("device", {}).get("type", "") return self.last_evidence.doc.get("device", {}).get("type", "")
if not self.last_evidence:
self.get_last_evidence()
return self.last_evidence.get_chassis() return self.last_evidence.get_chassis()
@property @property
def model(self): def model(self):
if not self.last_evidence: self.get_last_evidence()
self.get_last_evidence()
return self.last_evidence.get_model() return self.last_evidence.get_model()
@property
def version(self):
self.get_last_evidence()
return self.last_evidence.get_version()
@property @property
def components(self): def components(self):
if not self.last_evidence: self.get_last_evidence()
self.get_last_evidence()
return self.last_evidence.get_components() return self.last_evidence.get_components()
@property
def did_document(self):
self.get_last_evidence()
return self.last_evidence.get_did_document()

View file

@ -2,11 +2,60 @@
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<div class="row"> <!-- Top bar buttons -->
<div class="col"> <div class="row">
<h3>{{ object.shortid }}</h3> <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> </div>
</div> </div>
</div>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
@ -15,10 +64,7 @@
<a href="#details" class="nav-link active" data-bs-toggle="tab" data-bs-target="#details">{% trans 'General details' %}</a> <a href="#details" class="nav-link active" data-bs-toggle="tab" data-bs-target="#details">{% trans 'General details' %}</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="#annotations" class="nav-link" data-bs-toggle="tab" data-bs-target="#annotations">{% trans 'User annotations' %}</a> <a href="#user_properties" class="nav-link" data-bs-toggle="tab" data-bs-target="#user_properties">{% trans 'Properties' %}</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>
<li class="nav-item"> <li class="nav-item">
<a href="#lots" class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">{% trans 'Lots' %}</a> <a href="#lots" class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">{% trans 'Lots' %}</a>
@ -29,207 +75,62 @@
<li class="nav-item"> <li class="nav-item">
<a href="#evidences" class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">{% trans 'Evidences' %}</a> <a href="#evidences" class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">{% trans 'Evidences' %}</a>
</li> </li>
{% if dpps %}
<li class="nav-item">
<a href="#dpps" class="nav-link" data-bs-toggle="tab" data-bs-target="#dpps">{% trans 'Dpps' %}</a>
</li>
{% endif %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'device:device_web' object.id %}" target="_blank">Web</a> <a class="nav-link" href="{% url 'device:device_web' object.id %}" target="_blank">Web</a>
</li> </li>
<li class="nav-item">
<a href="#log" class="nav-link" data-bs-toggle="tab" data-bs-target="#log">{% trans 'Log' %}</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="tab-content pt-4">
<div class="tab-content pt-2"> {% include 'tabs/general_details.html' %}
<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>
{% if object.is_eraseserver %} {% include 'tabs/log.html' %}
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label"> {% include 'tabs/user_properties.html' %}
{% trans 'Is a erase server' %}
</div> {% include 'tabs/lots.html' %}
<div class="col-lg-9 col-md-8"></div>
{% include 'tabs/components.html' %}
{% include 'tabs/evidences.html' %}
{% include 'tabs/dpps.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>
</div> </div>
{% endif %} <div class="modal-body">
<form method="post" action="{% url 'action:add_note' %}">
<div class="row mb-3"> {% csrf_token %}
<div class="col-lg-3 col-md-4 label">Type</div> <div class="mb-3">
<div class="col-lg-9 col-md-8">{{ object.type }}</div> <input type="hidden" name="snapshot_uuid" value="{{ object.last_uuid }}">
</div> <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>
{% if object.is_websnapshot and object.last_user_evidence %}
{% 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.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 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> </div>
{% endif %} <div class="modal-footer">
{% endfor %} <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
{% endfor %} <button type="submit" class="btn btn-green-admin">{% trans "Save Note" %}</button>
</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> </div>
<p class="mb-1"> </form>
{% for k, v in c.items %} </div>
{% 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>
</div> </div>
{% endblock %} {% endblock %}
{% block extrascript %} {% block extrascript %}
@ -237,12 +138,12 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Obtener el hash de la URL (ejemplo: #components) // Obtener el hash de la URL (ejemplo: #components)
const hash = window.location.hash const hash = window.location.hash
// Verificar si hay un hash en la URL // Verificar si hay un hash en la URL
if (hash) { if (hash) {
// Buscar el botón o enlace que corresponde al hash y activarlo // Buscar el botón o enlace que corresponde al hash y activarlo
const tabTrigger = document.querySelector(`[data-bs-target="${hash}"]`) const tabTrigger = document.querySelector(`[data-bs-target="${hash}"]`)
if (tabTrigger) { if (tabTrigger) {
// Crear una instancia de tab de Bootstrap para activar el tab // Crear una instancia de tab de Bootstrap para activar el tab
const tab = new bootstrap.Tab(tabTrigger) const tab = new bootstrap.Tab(tabTrigger)

View file

@ -36,7 +36,7 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="container"> <div class="container">
<a class="btn btn-grey" href="{% url 'dashboard:unassigned_devices' %}">{% translate "Cancel" %}</a> <a class="btn btn-grey" href="{% url 'device:details' pk %}#user_properties">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" /> <input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div> </div>

View file

@ -1,251 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col">
<h3>{{ object.pk }}</h3>
</div>
</div>
<div class="row">
<div class="col">
<div class="nav nav-tabs nav-tabs-bordered">
<li class="nav-items">
<a class="nav-link" href="{% url 'device:details' device.pk %}">General details</a>
</li>
<li class="nav-items">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#physicalproperties">Physical properties</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="#status">Status</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="#traceabiliy">Traceability log</button>
</li>
<li class="nav-items">
<a class="nav-link" href="">Web</a>
</li>
</div>
</div>
</div>
<div class="tab-content pt-2">
<div class="tab-pane fade profile-overview" id="details">
<h5 class="card-title">Details</h5>
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label ">
(<a href="/inventory/device/edit/4W8D3/">Edit Device</a>)
</div>
<div class="col-lg-9 col-md-8">
{%if object.hid %}Snapshot{% else %}Placeholder{% endif %}
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label ">Phid</div>
<div class="col-lg-9 col-md-8">{{ object.id }}</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label ">Id device internal</div>
<div class="col-lg-9 col-md-8"></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>
<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">Part Number</div>
<div class="col-lg-9 col-md-8">{{ object.part_number|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.serial_number|default:"" }}</div>
</div>
</div>
<div class="tab-pane fade show active" id="physicalproperties">
<h5 class="card-title">Physical Properties</h5>
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label ">
(<a href="{% url 'device:physical_edit' object.pk %}">Edit Physical Properties</a>)
</div>
</div>
<div class="row mb-3">
<div class="col-lg-3 col-md-4 label ">
{% load django_bootstrap5 %}
<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="form-actions-no-box">
<a class="btn btn-grey" href="{% url 'device:details' device.pk %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>
</form>
</div>
</div>
</div>
<div class="tab-pane fade profile-overview" id="lots">
<h5 class="card-title">Incoming Lots</h5>
<div class="row">
</div>
<h5 class="card-title">Outgoing Lots</h5>
<div class="row">
</div>
<h5 class="card-title">Temporary Lots</h5>
<div class="row">
</div>
</div>
<div class="tab-pane fade profile-overview" id="documents">
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
<a href="/inventory/device/4W8D3/document/add/" class="btn btn-primary">
<i class="bi bi-plus"></i>
Add new document
<span class="caret"></span>
</a>
</div>
<h5 class="card-title">Documents</h5>
<table class="table">
<thead>
<tr>
<th scope="col">File</th>
<th scope="col">Type</th>
<th scope="col">Description</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm">Uploaded on</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="tab-pane fade profile-overview" id="status">
<h5 class="card-title">Status Details</h5>
<div class="row">
<div class="col-lg-3 col-md-4 label">Physical State</div>
<div class="col-lg-9 col-md-8">
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Lifecycle State</div>
<div class="col-lg-9 col-md-8">
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Allocated State</div>
<div class="col-lg-9 col-md-8">
</div>
</div>
</div>
<div class="tab-pane fade profile-overview" id="traceability">
<h5 class="card-title">Traceability log Details</h5>
<div class="list-group col-6">
<div class="list-group-item d-flex justify-content-between align-items-center">
Snapshot ✓
<small class="text-muted">14:07 23-06-2024</small>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
EraseCrypto ✓
<small class="text-muted">14:07 23-06-2024</small>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
EraseCrypto ✓
<small class="text-muted">14:07 23-06-2024</small>
</div>
</div>
</div>
<div class="tab-pane fade profile-overview" id="components">
<h5 class="card-title">Components Snapshot</h5>
<div class="list-group col-6">
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Motherboard</h5>
<small class="text-muted">14:07 23-06-2024</small>
</div>
<p class="mb-1">
hp<br />
890e<br />
</p>
<small class="text-muted">
</small>
</div>
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">NetworkAdapter</h5>
<small class="text-muted">14:07 23-06-2024</small>
</div>
<p class="mb-1">
realtek semiconductor co., ltd.<br />
rtl8852ae 802.11ax pcie wireless network adapter<br />
</p>
<small class="text-muted">
</small>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,27 @@
{% 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

@ -0,0 +1,18 @@
{% load i18n %}
<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.2.timestamp }}</small>
<span>{{ d.2.type }}</span>
</div>
<p class="mb-1">
<a href="{% url 'did:device_web' d.0 %}">{{ d.1 }}...</a>
</p>
</div>
{% endfor %}
</div>
</div>

View file

@ -0,0 +1,34 @@
{% load i18n %}
<div class="tab-pane fade" id="evidences">
<h5 class="card-title">{% trans 'List of evidences' %}</h5>
<div class="list-group col">
<table class="table">
<thead>
<tr>
<th scope="col">uuid</th>
<th scope="col">Did Document</th>
<th scope="col">{% trans "Date" %}</th>
</tr>
</thead>
<tbody>
{% for snap in object.evidences %}
<tr>
<td>
<a href="{% url 'evidence:details' snap.uuid %}">{{ snap.uuid }}</a>
</td>
<td>
{% if snap.did_document %}
<a href="{{ snap.did_document }}" target="_blank">DID</a>
{% endif %}
</td>
<td>
<small class="text-muted">{{ snap.created }}</small>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View file

@ -0,0 +1,81 @@
{% 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

@ -0,0 +1,28 @@
{% 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

@ -0,0 +1,19 @@
{% 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

@ -0,0 +1,131 @@
{% 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' object.id 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' object.id 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,8 +7,11 @@ urlpatterns = [
path("add/", views.NewDeviceView.as_view(), name="add"), path("add/", views.NewDeviceView.as_view(), name="add"),
path("edit/<str:pk>/", views.EditDeviceView.as_view(), name="edit"), path("edit/<str:pk>/", views.EditDeviceView.as_view(), name="edit"),
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>/user_property/add",
path("<str:pk>/document/add", views.AddDocumentView.as_view(), name="add_document"), views.AddUserPropertyView.as_view(), name="add_user_property"),
path("<str:device_id>/user_property/<int:pk>/delete",
views.DeleteUserPropertyView.as_view(), name="delete_user_property"),
path("<str:device_id>/user_property/<int:pk>/update",
views.UpdateUserPropertyView.as_view(), name="update_user_property"),
path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web"), path("<str:pk>/public/", views.PublicDeviceWebView.as_view(), name="device_web"),
] ]

View file

@ -1,23 +1,41 @@
import json import json
from django.http import JsonResponse import logging
from django.http import Http404 from django.http import JsonResponse
from django.conf import settings
from django.db import IntegrityError
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404, Http404 from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic.edit import ( from django.views.generic.edit import (
CreateView, CreateView,
UpdateView, UpdateView,
FormView, FormView,
DeleteView,
) )
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from action.models import StateDefinition, State, DeviceLog, Note
from dashboard.mixins import DashboardView, Http403 from dashboard.mixins import DashboardView, Http403
from evidence.models import Annotation from evidence.models import UserProperty, SystemProperty
from lot.models import LotTag from lot.models import LotTag
from device.models import Device from device.models import Device
from device.forms import DeviceFormSet from device.forms import DeviceFormSet
if settings.DPP:
from dpp.models import Proof
from dpp.api_dlt import PROOF_TYPE
class DeviceLogMixin(DashboardView):
def log_registry(self, _uuid, msg):
DeviceLog.objects.create(
snapshot_uuid=_uuid,
event=msg,
user=self.request.user,
institution=self.request.user.institution
)
class NewDeviceView(DashboardView, FormView): class NewDeviceView(DashboardView, FormView):
template_name = "new_device.html" template_name = "new_device.html"
title = _("New Device") title = _("New Device")
@ -35,41 +53,12 @@ class NewDeviceView(DashboardView, FormView):
return response return response
# class AddToLotView(DashboardView, FormView):
# template_name = "list_lots.html"
# title = _("Add to lots")
# breadcrumb = "lot / add to lots"
# success_url = reverse_lazy('dashboard:unassigned_devices')
# form_class = LotsForm
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# lots = Lot.objects.filter(owner=self.request.user)
# lot_tags = LotTag.objects.filter(owner=self.request.user)
# context.update({
# 'lots': lots,
# 'lot_tags':lot_tags,
# })
# return context
# def get_form(self):
# form = super().get_form()
# form.fields["lots"].queryset = Lot.objects.filter(owner=self.request.user)
# return form
# def form_valid(self, form):
# form.devices = self.get_session_devices()
# form.save()
# response = super().form_valid(form)
# return response
class EditDeviceView(DashboardView, UpdateView): class EditDeviceView(DashboardView, UpdateView):
template_name = "new_device.html" template_name = "new_device.html"
title = _("Update Device") title = _("Update Device")
breadcrumb = "Device / Update Device" breadcrumb = "Device / Update Device"
success_url = reverse_lazy('dashboard:unassigned_devices') success_url = reverse_lazy('dashboard:unassigned_devices')
model = Annotation model = SystemProperty
def get_form_kwargs(self): def get_form_kwargs(self):
pk = self.kwargs.get('pk') pk = self.kwargs.get('pk')
@ -87,7 +76,7 @@ class DetailsView(DashboardView, TemplateView):
template_name = "details.html" template_name = "details.html"
title = _("Device") title = _("Device")
breadcrumb = "Device / Details" breadcrumb = "Device / Details"
model = Annotation model = SystemProperty
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.pk = kwargs['pk'] self.pk = kwargs['pk']
@ -103,10 +92,34 @@ class DetailsView(DashboardView, TemplateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
self.object.initial() self.object.initial()
lot_tags = LotTag.objects.filter(owner=self.request.user.institution) lot_tags = LotTag.objects.filter(owner=self.request.user.institution)
dpps = []
if settings.DPP:
_dpps = Proof.objects.filter(
uuid__in=self.object.uuids,
type=PROOF_TYPE["IssueDPP"]
)
for x in _dpps:
dpp = "{}:{}".format(self.pk, x.signature)
dpps.append((dpp, x.signature[:10], x))
last_evidence = self.object.get_last_evidence()
uuids = self.object.uuids
state_definitions = StateDefinition.objects.filter(
institution=self.request.user.institution
).order_by('order')
device_states = State.objects.filter(snapshot_uuid__in=uuids).order_by('-date')
device_logs = DeviceLog.objects.filter(
snapshot_uuid__in=uuids).order_by('-date')
device_notes = Note.objects.filter(snapshot_uuid__in=uuids).order_by('-date')
context.update({ context.update({
'object': self.object, 'object': self.object,
'snapshot': self.object.get_last_evidence(), 'snapshot': last_evidence,
'lot_tags': lot_tags, 'lot_tags': lot_tags,
'dpps': dpps,
"state_definitions": state_definitions,
"device_states": device_states,
"device_logs": device_logs,
"device_notes": device_notes,
}) })
return context return context
@ -167,67 +180,120 @@ class PublicDeviceWebView(TemplateView):
return JsonResponse(device_data) return JsonResponse(device_data)
class AddAnnotationView(DashboardView, CreateView): class AddUserPropertyView(DeviceLogMixin, CreateView):
template_name = "new_annotation.html" template_name = "new_user_property.html"
title = _("New annotation") title = _("New User Property")
breadcrumb = "Device / New annotation" breadcrumb = "Device / New Property"
success_url = reverse_lazy('dashboard:unassigned_devices') model = UserProperty
model = Annotation
fields = ("key", "value") fields = ("key", "value")
def form_valid(self, form): def form_valid(self, form):
form.instance.owner = self.request.user.institution form.instance.owner = self.request.user.institution
form.instance.user = self.request.user form.instance.user = self.request.user
form.instance.uuid = self.annotation.uuid form.instance.uuid = self.property.uuid
form.instance.type = Annotation.Type.USER form.instance.type = UserProperty.Type.USER
response = super().form_valid(form)
return response try:
response = super().form_valid(form)
messages.success(self.request, _("Property successfully added."))
log_message = _("<Created> UserProperty: {}: {}".format(
form.instance.key,
form.instance.value
))
self.log_registry(form.instance.uuid, log_message)
return response
except IntegrityError:
messages.error(self.request, _("Property is already defined."))
return self.form_invalid(form)
def get_form_kwargs(self): def get_form_kwargs(self):
pk = self.kwargs.get('pk') pk = self.kwargs.get('pk')
institution = self.request.user.institution institution = self.request.user.institution
self.annotation = Annotation.objects.filter( self.property = SystemProperty.objects.filter(
owner=institution, owner=institution, value=pk).first()
value=pk, if not self.property:
type=Annotation.Type.SYSTEM
).first()
if not self.annotation:
raise Http404 raise Http404
self.success_url = reverse_lazy('device:details', args=[pk]) return super().get_form_kwargs()
kwargs = super().get_form_kwargs()
return kwargs def get_success_url(self):
pk = self.kwargs.get('pk')
return reverse_lazy('device:details', args=[pk]) + "#user_properties"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['pk'] = self.kwargs.get('pk')
return context
class AddDocumentView(DashboardView, CreateView): class UpdateUserPropertyView(DeviceLogMixin, UpdateView):
template_name = "new_annotation.html" template_name = "new_user_property.html"
title = _("New Document") title = _("Update User Property")
breadcrumb = "Device / New document" breadcrumb = "Device / Update Property"
success_url = reverse_lazy('dashboard:unassigned_devices') model = UserProperty
model = Annotation
fields = ("key", "value") 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): def get_form_kwargs(self):
pk = self.kwargs.get('pk') pk = self.kwargs.get('pk')
institution = self.request.user.institution institution = self.request.user.institution
self.annotation = Annotation.objects.filter( self.object = get_object_or_404(UserProperty, owner=institution, pk=pk)
owner=institution, self.old_key = self.object.key
value=pk, self.old_value = self.object.value
type=Annotation.Type.SYSTEM return super().get_form_kwargs()
).first()
if not self.annotation: def form_valid(self, form):
raise Http404 new_key = form.cleaned_data['key']
new_value = form.cleaned_data['value']
self.success_url = reverse_lazy('device:details', args=[pk]) try:
kwargs = super().get_form_kwargs() super().form_valid(form)
return kwargs messages.success(self.request, _("Property updated successfully."))
log_message = _("<Updated> UserProperty: {}: {} to {}: {}".format(
self.old_key,
self.old_value,
new_key,
new_value
))
self.log_registry(form.instance.uuid, log_message)
# return response
return redirect(self.get_success_url())
except IntegrityError:
messages.error(self.request, _("Property is already defined."))
return self.form_invalid(form)
def form_invalid(self, form):
super().form_invalid(form)
return redirect(self.get_success_url())
def get_success_url(self):
pk = self.kwargs.get('device_id')
return reverse_lazy('device:details', args=[pk]) + "#user_properties"
class DeleteUserPropertyView(DeviceLogMixin, 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):
pk = self.kwargs.get('pk')
institution = self.request.user.institution
self.object = get_object_or_404(UserProperty, owner=institution, pk=pk)
self.object.delete()
msg = _("<Deleted> User Property: {}:{}".format(
self.object.key,
self.object.value
))
self.log_registry(self.object.uuid, msg)
messages.info(self.request, _("User property deleted successfully."))
return redirect(self.get_success_url())
def get_success_url(self):
pk = self.kwargs.get('device_id')
return reverse_lazy('device:details', args=[pk]) + "#user_properties"

View file

@ -65,6 +65,7 @@ ENABLE_EMAIL = config("ENABLE_EMAIL", default=True, cast=bool)
EVIDENCES_DIR = config("EVIDENCES_DIR", default=os.path.join(BASE_DIR, "db")) EVIDENCES_DIR = config("EVIDENCES_DIR", default=os.path.join(BASE_DIR, "db"))
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
@ -76,22 +77,23 @@ INSTALLED_APPS = [
"django.contrib.staticfiles", "django.contrib.staticfiles",
'django_extensions', 'django_extensions',
'django_bootstrap5', 'django_bootstrap5',
'fontawesomefree',
'django_tables2', 'django_tables2',
"rest_framework",
"login", "login",
"user", "user",
"device", "device",
"evidence", "evidence",
"action",
"tag",
"lot", "lot",
"documents",
"dashboard", "dashboard",
"action",
"admin", "admin",
"api", "api",
] ]
DPP = config("DPP", default=False, cast=bool)
if DPP:
INSTALLED_APPS.extend(["dpp", "did"])
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
@ -211,6 +213,10 @@ LOGGING = {
'()': CustomFormatter, '()': CustomFormatter,
'format': '%(levelname)s %(asctime)s %(message)s' 'format': '%(levelname)s %(asctime)s %(message)s'
}, },
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
}, },
"handlers": { "handlers": {
"console": { "console": {
@ -233,10 +239,16 @@ LOGGING = {
"handlers": ["console"], "handlers": ["console"],
"level": "ERROR", "level": "ERROR",
"propagate": False, "propagate": False,
} },
} }
} }
SNAPSHOT_PATH="/tmp/" SNAPSHOT_PATH="/tmp/"
DATA_UPLOAD_MAX_NUMBER_FILES = 1000 DATA_UPLOAD_MAX_NUMBER_FILES = 1000
COMMIT = config('COMMIT', default='') COMMIT = config('COMMIT', default='')
# DLT SETTINGS
TOKEN_DLT = config("API_DLT_TOKEN", default=None)
API_DLT = config("API_DLT", default=None)
API_RESOLVER = config("API_RESOLVER", default=None)
ID_FEDERATED = config("ID_FEDERATED", default=None)

View file

@ -14,7 +14,7 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf import settings
from django.urls import path, include from django.urls import path, include
urlpatterns = [ urlpatterns = [
@ -22,9 +22,16 @@ urlpatterns = [
path("", include("login.urls")), path("", include("login.urls")),
path("dashboard/", include("dashboard.urls")), path("dashboard/", include("dashboard.urls")),
path("evidence/", include("evidence.urls")), path("evidence/", include("evidence.urls")),
path('action/', include('action.urls')),
path("device/", include("device.urls")), path("device/", include("device.urls")),
path("admin/", include("admin.urls")), path("admin/", include("admin.urls")),
path("user/", include("user.urls")), path("user/", include("user.urls")),
path("lot/", include("lot.urls")), path("lot/", include("lot.urls")),
path('api/', include('api.urls')), path('api/', include('api.urls')),
] ]
if settings.DPP:
urlpatterns.extend([
path('dpp/', include('dpp.urls')),
path('did/', include('did.urls')),
])

View file

@ -1,6 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
class TagConfig(AppConfig): class DidConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "tag" name = "did"

View file

@ -0,0 +1,41 @@
dpp_tmpl = {
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://test.uncefact.org/vocabulary/untp/dpp/0.5.0/"
],
"type": [
"DigitalProductPassport",
"VerifiableCredential"
],
"id": "https://example.ereuse.org/credentials/2a423366-a0d6-4855-ba65-2e0c926d09b0",
"issuer": {
"type": [
"CredentialIssuer"
],
"id": "did:web:r1.identifiers.ereuse.org:did-registry:z6Mkoreij5y9bD9fL5SGW6TfMUmcbaV7LCPwZHCFEEZBrVYQ#z6Mkoreij5y9bD9fL5SGW6TfMUmcbaV7LCPwZHCFEEZBrVYQ",
"name": "Refurbisher One"
},
"validFrom": "2024-11-15T12:00:00",
"validUntil": "2034-11-15T12:00:00",
"credentialSubject": {
"type": [
"Product"
],
"id": "https://id.ereuse.org/01/09520123456788/21/12345",
"name": "Refurbished XYZ Lenovo laptop item",
"registeredId": "09520123456788.21.12345",
"description": "XYZ Lenovo laptop refurbished by Refurbisher One",
"data": ""
},
"credentialSchema": {
"id": "https://idhub.pangea.org/vc_schemas/dpp.json",
"type": "FullJsonSchemaValidator2021",
"proof": {
"type": "Ed25519Signature2018",
"proofPurpose": "assertionMethod",
"verificationMethod": "did:web:r1.identifiers.ereuse.org:did-registry:z6Mkoreij5y9bD9fL5SGW6TfMUmcbaV7LCPwZHCFEEZBrVYQ#z6Mkoreij5y9bD9fL5SGW6TfMUmcbaV7LCPwZHCFEEZBrVYQ",
"created": "2024-12-03T15:33:42Z",
"jws": "eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..rBPqbOcZCXB7GAnq1XIfV9Jvw4MKXlHff7qZkRfgwQ0Hnd9Ujt5s1xT4O0K6VESzWvdP2mOvMvu780fVNfraBQ"
}
}
}

View file

@ -0,0 +1,497 @@
<!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">
<nav class="header-nav ms-auto">
<div class="d-flex align-items-right">
<span class="nav-item">
{% if not roles and user.is_anonymous %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#validateModal">Validate</button>
{% else %}
<button class="btn btn-primary" id="buttonRole" data-bs-toggle="modal" data-bs-target="#rolesModal">Select your role</button>
<a class="btn btn-primary" href="{% url 'login:logout' %}?next={{ path }}">Logout</a>
{% endif %}
</span>
</div>
{% if role %}
<div class="d-flex justify-content-end">
<span class="nav-item">
Current Role: {{ role }}
</span>
</div>
{% endif %}
</nav>
<h1 class="text-center mb-4" style="color: #545f71;">{{ object.manufacturer }} {{ object.type }} {{ object.model }}</h1>
<div class="row">
<div class="col-lg-6">
{% if manuals.details.logo %}
<img style="max-width: 50px; margin-right: 15px;" src="{{ manuals.details.logo }}" />
{% endif %}
</div>
<div class="col-lg-6">
{% if manuals.details.image %}
<img style="width: 100px;" src="{{ manuals.details.image }}" />
{% endif %}
</div>
</div>
<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>
{% if user.is_authenticated %}
<div class="info-row row">
<div class="col-md-4 info-label">Serial Number</div>
<div class="col-md-8 info-value">{{ object.serial_number|default:'' }}</div>
</div>
{% endif %}
{% 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' %}
{% if component_key != 'serialNumber' or user.is_authenticated %}
<strong>{{ component_key }}:</strong> {{ component_value }}<br />
{% endif %}
{% endif %}
{% endfor %}
</p>
</div>
</div>
</div>
{% endfor %}
</div>
{% if manuals.icecat %}
<h5 class="card-title">Icecat data sheet</h5>
<div class="row">
<div class="col-12 list-group-item d-flex align-items-center">
{% if manuals.details.logo %}
<img style="max-width: 50px; margin-right: 15px;" src="{{ manuals.details.logo }}" />
{% endif %}
{% if manuals.details.image %}
<img style="max-width: 100px; margin-right: 15px;" src="{{ manuals.details.image }}" />
{% endif %}
{% if manuals.details.pdf %}
<a href="{{ manuals.details.pdf }}" target="_blank">{{ manuals.details.title }}</a><br />
{% else %}
{{ manuals.details.title }}<br />
{% endif %}
</div>
<div class="col-12 accordion-item">
<h5 class="card-title accordion-header">
<button class="accordion-button collapsed" data-bs-target="#manuals-icecat" type="button"
data-bs-toggle="collapse" aria-expanded="false">
More examples
</button>
</h5>
<div id="manuals-icecat" class="row accordion-collapse collapse">
<div class="accordion-body">
{% for m in manuals.icecat %}
<div class="list-group-item d-flex align-items-center">
{% if m.logo %}
<img style="max-width: 50px; margin-right: 15px;" src="{{ m.logo }}" />
{% endif %}
{% if m.pdf %}
<a href="{{ m.pdf }}" target="_blank">{{ m.title }}</a><br />
{% else %}
{{ m.title }}<br />
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
{% if manuals.laer %}
<div class="row mt-3">
<div class="col-12">
<h5 class="card-title">Recycled Content</h5>
<div class="row mb-3">
<div class="col-sm-2">
Metal
</div>
<div class="col-sm-10">
<div class="progress">
<div class="progress-bar"
role="progressbar"
style="width: {{ manuals.laer.0.metal }}%"
aria-valuenow="{{ manuals.laer.0.metal }}"
aria-valuemin="0"
aria-valuemax="100">{{ manuals.laer.0.metal }}%
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-2">
Plastic post Consumer
</div>
<div class="col-sm-10">
<div class="progress">
<div class="progress-bar"
role="progressbar"
style="width: {{ manuals.laer.0.plastic_post_consumer }}%"
aria-valuenow="{{ manuals.laer.0.plastic_post_consumer }}"
aria-valuemin="0"
aria-valuemax="100">{{ manuals.laer.0.plastic_post_consumer }}%
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-2">
Plastic post Industry
</div>
<div class="col-sm-10">
<div class="progress">
<div class="progress-bar"
role="progressbar"
style="width: {{ manuals.laer.0.plastic_post_industry }}%"
aria-valuenow="{{ manuals.laer.0.plastic_post_industry }}"
aria-valuemin="0"
aria-valuemax="100">{{ manuals.laer.0.plastic_post_industry }}%
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if manuals.energystar %}
<div class="row mt-3">
<div class="col-12">
<h5 class="card-title">Energy spent</h5>
{% if manuals.energystar.long_idle_watts %}
<div class="row mb-3">
<div class="col-sm-10">
Consumption when inactivity power function is activated (watts)
</div>
<div class="col-sm-2">
{{ manuals.energystar.long_idle_watts }}
</div>
</div>
{% endif %}
{% if manuals.energystar.short_idle_watts %}
<div class="row mb-3">
<div class="col-sm-10">
Consumption when inactivity power function is not activated (watts)
</div>
<div class="col-sm-2">
{{ manuals.energystar.short_idle_watts }}
</div>
</div>
{% endif %}
{% if manuals.energystar.sleep_mode_watts %}
<div class="row mb-3">
<div class="col-sm-10">
sleep_mode_watts
Consumption when computer goes into sleep mode (watts)
</div>
<div class="col-sm-2">
{{ manuals.energystar.sleep_mode_watts }}
</div>
</div>
{% endif %}
{% if manuals.energystar.off_mode_watts %}
<div class="row mb-3">
<div class="col-sm-10">
Consumption when the computer is off (watts)
</div>
<div class="col-sm-2">
{{ manuals.energystar.off_mode_watts }}
</div>
</div>
{% endif %}
{% if manuals.energystar.tec_allowance_kwh %}
<div class="row mb-3">
<div class="col-sm-10">
Power allocation for normal operation (kwh)
</div>
<div class="col-sm-2">
{{ manuals.energystar.tec_allowance_kwh }}
</div>
</div>
{% endif %}
{% if manuals.energystar.tec_of_model_kwh %}
<div class="row mb-3">
<div class="col-sm-10">
Consumption of the model configuration (kwh)
</div>
<div class="col-sm-2">
{{ manuals.energystar.tec_of_model_kwh }}
</div>
</div>
{% endif %}
{% if manuals.energystar.tec_requirement_kwh %}
<div class="row mb-3">
<div class="col-sm-10">
Energy allowance provided (kwh)
</div>
<div class="col-sm-2">
{{ manuals.energystar.tec_requirement_kwh }}
</div>
</div>
{% endif %}
{% if manuals.energystar.work_off_mode_watts %}
<div class="row mb-3">
<div class="col-sm-10">
The lowest power mode which cannot be switched off (watts)
</div>
<div class="col-sm-2">
{{ manuals.energystar.work_off_mode_watts }}
</div>
</div>
{% endif %}
{% if manuals.energystar.work_weighted_power_of_model_watts %}
<div class="row mb-3">
<div class="col-sm-10">
Weighted energy consumption from all its states (watts)
</div>
<div class="col-sm-2">
{{ manuals.energystar.work_weighted_power_of_model_watts }}
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
{% if manuals.ifixit %}
<div class="row">
<div class="col-12 accordion-item">
<h5 class="card-title accordion-header">
<button class="accordion-button collapsed" data-bs-target="#manuals-repair" type="button"
data-bs-toggle="collapse" aria-expanded="false">
Repair manuals
</button>
</h5>
<div id="manuals-repair" class="row accordion-collapse collapse">
<div class="list-group col">
{% for m in manuals.ifixit %}
<div class="list-group-item d-flex align-items-center">
{% if m.image %}
<img style="max-width: 100px; margin-right: 15px;" src="{{ m.image }}" />
{% endif %}
{% if m.url %}
<a href="{{ m.url }}" target="_blank">{{ m.title }}</a><br />
{% else %}
{{ m.title }}<br />
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
<footer>
<p>
&copy;{% now 'Y' %}eReuse. All rights reserved.
</p>
</footer>
{% if user.is_anonymous and not roles %}
<div class="modal fade" id="validateModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Validate as <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<a class="btn btn-primary" type="button"
href="{% url 'login:login' %}?next={{ path }}">
User of system
</a>
{% if oidc %}
<br />
<a class="btn btn-primary mt-3" type="button" href="{# url 'oidc:login_other_inventory' #}?next={{ path }}">
User of other inventory
</a>
{% endif %}
</div>
<div class="modal-footer"></div>
</div>
</div>
</div>
{% else %}
<div class="modal fade" id="rolesModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form action="{{ path }}" method="get">
<div class="modal-header">
<h5 class="modal-title">Select your Role <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<select name="role">
{% for k, v in roles %}
<option value="{{ k }}" {% if v == role %}selected=selected{% endif %}>{{ v }}</option>
{% endfor %}
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" value="Send" />
</div>
</form>
</div>
</div>
</div>
{% endif %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
</body>
</html>

8
did/urls.py Normal file
View file

@ -0,0 +1,8 @@
from django.urls import path
from did import views
app_name = 'did'
urlpatterns = [
path("<str:pk>", views.PublicDeviceWebView.as_view(), name="device_web"),
]

263
did/views.py Normal file
View file

@ -0,0 +1,263 @@
import json
import logging
from django.http import JsonResponse, Http404
from django.views.generic.base import TemplateView
from device.models import Device
from evidence.parse import Build
from dpp.api_dlt import ALGORITHM
from dpp.models import Proof
from dpp.api_dlt import PROOF_TYPE
from did.template_credential import dpp_tmpl
logger = logging.getLogger('django')
class PublicDeviceWebView(TemplateView):
template_name = "device_did.html"
def get(self, request, *args, **kwargs):
self.pk = kwargs['pk']
chid = self.pk.split(":")[0]
proof = Proof.objects.filter(signature=self.pk).first()
if proof:
self.object = Device(id=chid, uuid=proof.uuid)
else:
self.object = Device(id=chid)
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):
self.context = super().get_context_data(**kwargs)
self.object.initial()
roles = [("Operator", "Operator")]
role = "Operator"
if self.request.user.is_anonymous:
roles = []
role = None
self.context.update({
'object': self.object,
'role': role,
'roles': roles,
'path': self.request.path,
'last_dpp': "",
'before_dpp': "",
})
if not self.request.user.is_anonymous:
self.get_manuals()
return self.context
@property
def public_fields(self):
return {
'id': self.object.id,
'shortid': self.object.shortid,
'uuids': self.object.uuids,
'hids': self.object.hids,
'components': self.remove_serial_number_from(self.object.components),
}
@property
def authenticated_fields(self):
return {
'serial_number': self.object.serial_number,
'components': self.object.components,
}
def remove_serial_number_from(self, components):
for component in components:
if 'serial_number' in component:
del component['SerialNumber']
return components
def get_device_data(self):
data = self.public_fields
if self.request.user.is_authenticated:
data.update(self.authenticated_fields)
return data
def get_json_response(self):
device_data = self.get_result()
# device_data = self.get_device_data()
response = JsonResponse(device_data)
response["Access-Control-Allow-Origin"] = "*"
return response
def get_result(self):
if len(self.pk.split(":")) > 1:
return self.build_from_dpp()
else:
return self.build_from_chid()
def build_from_dpp(self):
data = {
'document': {},
'dpp': self.pk,
'algorithm': ALGORITHM,
'components': [],
'manufacturer DPP': '',
'device': {},
}
dev = Build(self.object.last_evidence.doc, None, check=True)
doc = dev.build.get_doc()
data['document'] = json.dumps(doc)
data['device'] = dev.build.device
data['components'] = dev.build.components
self.object.get_evidences()
last_dpp = Proof.objects.filter(
uuid__in=self.object.uuids, type=PROOF_TYPE['IssueDPP']
).order_by("-timestamp").first()
key = self.pk
if last_dpp:
key += ":"+last_dpp.signature
url = "https://{}/did/{}".format(
self.request.get_host(),
key
)
data['url_last'] = url
tmpl = dpp_tmpl.copy()
tmpl["credentialSubject"]["data"] = data
return tmpl
def build_from_chid(self):
dpps = []
self.object.initial()
for d in self.object.evidences:
d.get_doc()
dev = Build(d.doc, None, check=True)
doc = dev.build.get_doc()
ev = json.dumps(doc)
phid = dev.sign(ev)
dpp = "{}:{}".format(self.pk, phid)
rr = {
'dpp': dpp,
'document': ev,
'algorithm': ALGORITHM,
'manufacturer DPP': '',
'device': dev.build.device,
'components': dev.build.components
}
tmpl = dpp_tmpl.copy()
tmpl["credentialSubject"]["data"] = rr
dpps.append(tmpl)
return {
'@context': ['https://ereuse.org/dpp0.json'],
'data': dpps,
}
def get_manuals(self):
manuals = {
'ifixit': [],
'icecat': [],
'details': {},
'laer': [],
'energystar': {},
}
try:
params = {
"manufacturer": self.object.manufacturer,
"model": self.object.model,
}
self.params = json.dumps(params)
manuals['ifixit'] = self.request_manuals('ifixit')
manuals['icecat'] = self.request_manuals('icecat')
manuals['laer'] = self.request_manuals('laer')
manuals['energystar'] = self.request_manuals('energystar') or {}
if manuals['icecat']:
manuals['details'] = manuals['icecat'][0]
except Exception as err:
logger.error("Error: {}".format(err))
self.context['manuals'] = manuals
self.parse_energystar()
def parse_energystar(self):
if not self.context.get('manuals', {}).get('energystar'):
return
# Defined in:
# https://dev.socrata.com/foundry/data.energystar.gov/j7nq-iepp
energy_types = [
'functional_adder_allowances_kwh',
'tec_allowance_kwh',
'long_idle_watts',
'short_idle_watts',
'off_mode_watts',
'sleep_mode_watts',
'tec_of_model_kwh',
'tec_requirement_kwh',
'work_off_mode_watts',
'work_weighted_power_of_model_watts',
]
energy = {}
for field in energy_types:
energy[field] = []
for e in self.context['manuals']['energystar']:
for field in energy_types:
for k, v in e.items():
if not v:
continue
if field in k:
energy[field].append(v)
for k, v in energy.items():
if not v:
energy[k] = 0
continue
tt = sum([float(i) for i in v])
energy[k] = round(tt / len(v), 2)
self.context['manuals']['energystar'] = energy
def request_manuals(self, prefix):
#TODO reimplement manuals service
response = {
"laer": [{"metal": 40, "plastic_post_consumer": 27, "plastic_post_industry": 34}],
"energystar": [{
'functional_adder_allowances_kwh': 180,
"long_idle_watts": 240,
"short_idle_watts": 120,
"sleep_mode_watts": 30,
"off_mode_watts": 3,
"tec_allowance_kwh": 180,
"tec_of_model_kwh": 150,
"tec_requirement_kwh": 220,
"work_off_mode_watts": 70,
"work_weighted_power_of_model_watts": 240
}],
"ifixit": [
{
"image": "https://guide-images.cdn.ifixit.com/igi/156EpI4YdQeVfVPa.medium",
"url": "https://es.ifixit.com/Gu%C3%ADa/HP+ProBook+450+G4+Back+Panel+Replacement/171196?lang=en",
"title": "HP ProBook 450 G4 Back Panel Replacement"
},
{
"image": "https://guide-images.cdn.ifixit.com/igi/usTIqCKpuxVWC3Ix.140x105",
"url": "https://es.ifixit.com/Gu%C3%ADa/HP+ProBook+450+G4+Display+Assembly+Replacement/171101?lang=en",
"title": "Display Assembly Replacement"
}
],
"icecat": [
{
"logo": "https://images.icecat.biz/img/brand/thumb/1_cf8603f6de7b4c4d8ac4f5f0ef439a05.jpg",
"image": "https://guide-images.cdn.ifixit.com/igi/Q2nYjTIQfG6GaI5B.standard",
"pdf": "https://icecat.biz/rest/product-pdf?productId=32951710&lang=en",
"title": "HP ProBook 450 G3"
}
]
}
return response.get(prefix, {})

View file

@ -1,16 +1,59 @@
services: services:
devicehub-django: devicehub-django:
init: true init: true
image: farga.pangea.org/ereuse/devicehub-django:latest
build: build:
context: .
dockerfile: docker/devicehub-django.Dockerfile dockerfile: docker/devicehub-django.Dockerfile
environment: environment:
- DEBUG=${DEBUG:-false} - DEBUG=${DEBUG:-false}
- DOMAIN=${DOMAIN:-localhost} - DOMAIN=${DEVICEHUB_DOMAIN:-localhost}
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-$DOMAIN} - PORT=${DEVICEHUB_PORT:-8000}
- ALLOWED_HOSTS=${DEVICEHUB_ALLOWED_HOSTS:-$DEVICEHUB_DOMAIN}
- DEMO=${DEMO:-false} - DEMO=${DEMO:-false}
- DEMO_IDHUB_DOMAIN=${DEMO_IDHUB_DOMAIN:-}
- DEMO_IDHUB_PREDEFINED_TOKEN=${IDHUB_PREDEFINED_TOKEN:-}
- PREDEFINED_TOKEN=${PREDEFINED_TOKEN:-} - PREDEFINED_TOKEN=${PREDEFINED_TOKEN:-}
- DPP=${DPP:-false}
# TODO manage volumes dev vs prod
volumes: volumes:
- .:/opt/devicehub-django - .:/opt/devicehub-django
ports: 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
- DEMO=${DEMO:-false}
- 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}
- PREDEFINED_TOKEN=${IDHUB_PREDEFINED_TOKEN:-}
- 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=['Snapshot']
- SYNC_ORG_DEV=${IDHUB_SYNC_ORG_DEV}
ports:
- 9001:9001
# TODO add database service for idhub, meanwhile sqlite

View file

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

View file

@ -6,9 +6,16 @@ RUN apt update && \
python3-xapian \ python3-xapian \
git \ git \
sqlite3 \ sqlite3 \
curl \
jq \ jq \
time \
vim \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# TODO I don't like this, but the whole ereuse-dpp works with user 1000 because of the volume mapping
# thanks https://stackoverflow.com/questions/70520205/docker-non-root-user-best-practices-for-python-images
RUN adduser --home /opt/devicehub-django -u 1000 app
WORKDIR /opt/devicehub-django WORKDIR /opt/devicehub-django
# reduce size (python specifics) -> src https://stackoverflow.com/questions/74616667/removing-pip-cache-after-installing-dependencies-in-docker-image # reduce size (python specifics) -> src https://stackoverflow.com/questions/74616667/removing-pip-cache-after-installing-dependencies-in-docker-image
@ -22,15 +29,19 @@ compile = no
no-cache-dir = True no-cache-dir = True
END END
# upgrade pip, which might fail on lxc, then remove the "corrupted file"
RUN python -m pip install --upgrade pip || (rm -rf /usr/local/lib/python3.11/site-packages/pip-*.dist-info && python -m pip install --upgrade pip)
COPY ./requirements.txt /opt/devicehub-django COPY ./requirements.txt /opt/devicehub-django
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
# TODO hardcoded, is ignored in requirements.txt
RUN pip install -i https://test.pypi.org/simple/ ereuseapitest==0.0.14
# TODO Is there a better way? # TODO Is there a better way?
# Set PYTHONPATH to include the directory with the xapian module # Set PYTHONPATH to include the directory with the xapian module
ENV PYTHONPATH="${PYTHONPATH}:/usr/lib/python3/dist-packages" ENV PYTHONPATH="${PYTHONPATH}:/usr/lib/python3/dist-packages"
COPY . .
COPY docker/devicehub-django.entrypoint.sh / COPY docker/devicehub-django.entrypoint.sh /
RUN chown -R app:app /opt/devicehub-django
USER app
ENTRYPOINT sh /devicehub-django.entrypoint.sh ENTRYPOINT sh /devicehub-django.entrypoint.sh

View file

@ -5,6 +5,181 @@ set -u
# DEBUG # DEBUG
set -x set -x
# TODO there is a conflict between two shared vars
# 1. from the original docker compose devicehub-teal
# 2. from the new docker compose that integrates all dpp services
wait_for_dpp_shared() {
while true; do
# specially ensure VERAMO_API_CRED_FILE is not empty,
# it takes some time to get data in
OPERATOR_TOKEN_FILE='operator-token.txt'
if [ -f "/shared/${OPERATOR_TOKEN_FILE}" ] && \
[ -f "/shared/create_user_operator_finished" ]; then
sleep 5
echo "Files ready to process."
break
else
echo "Waiting for file in shared: ${OPERATOR_TOKEN_FILE}"
sleep 5
fi
done
}
# 3. Generate an environment .env file.
# TODO cargar via shared
gen_env_vars() {
INIT_ORG="${INIT_ORG:-example-org}"
INIT_USER="${INIT_USER:-user@example.org}"
INIT_PASSWD="${INIT_PASSWD:-1234}"
ADMIN='True'
PREDEFINED_TOKEN="${PREDEFINED_TOKEN:-}"
# specific dpp env vars
if [ "${DPP:-}" = 'true' ]; then
# fill env vars in this docker entrypoint
wait_for_dpp_shared
export API_DLT='http://api_connector:3010'
export API_DLT_TOKEN="$(cat "/shared/${OPERATOR_TOKEN_FILE}")"
export API_RESOLVER='http://id_index_api:3012'
# TODO hardcoded
export ID_FEDERATED='DH1'
fi
}
handle_federated_id() {
# devicehub host and id federated checker
# //getAll queries are not accepted by this service, so we remove them
EXPECTED_ID_FEDERATED="$(curl -s "${API_RESOLVER%/}/getAll" \
| jq -r '.url | to_entries | .[] | select(.value == "'"${DEVICEHUB_HOST}"'") | .key' \
| head -n 1)"
# if is a new DEVICEHUB_HOST, then register it
if [ -z "${EXPECTED_ID_FEDERATED}" ]; then
# TODO better docker compose run command
cmd="docker compose run --entrypoint= devicehub flask dlt_insert_members ${DEVICEHUB_HOST}"
big_error "No FEDERATED ID maybe you should run \`${cmd}\`"
fi
# if not new DEVICEHUB_HOST, then check consistency
# if there is already an ID in the DLT, it should match with my internal ID
if [ ! "${EXPECTED_ID_FEDERATED}" = "${ID_FEDERATED}" ]; then
big_error "ID_FEDERATED should be ${EXPECTED_ID_FEDERATED} instead of ${ID_FEDERATED}"
fi
# not needed, but reserved
# EXPECTED_DEVICEHUB_HOST="$(curl -s "${API_RESOLVER%/}/getAll" \
# | jq -r '.url | to_entries | .[] | select(.key == "'"${ID_FEDERATED}"'") | .value' \
# | head -n 1)"
# if [ ! "${EXPECTED_DEVICEHUB_HOST}" = "${DEVICEHUB_HOST}" ]; then
# big_error "ERROR: DEVICEHUB_HOST should be ${EXPECTED_DEVICEHUB_HOST} instead of ${DEVICEHUB_HOST}"
# fi
}
config_dpp_part1() {
# 12. Add a new server to the 'api resolver'
if [ "${ID_SERVICE:-}" ]; then
handle_federated_id
else
# TODO when this runs more than one time per service, this is a problem, but for the docker-reset.sh workflow, that's fine
# TODO put this in already_configured
# TODO hardcoded http proto and port
./manage.py dlt_insert_members "http://${DOMAIN}:8000"
fi
# 13. Do a rsync api resolve
./manage.py dlt_rsync_members
# 14. Register a new user to the DLT
DATASET_FILE='/tmp/dataset.json'
cat > "${DATASET_FILE}" <<END
{
"email": "${INIT_USER}",
"password": "${INIT_PASSWD}",
"api_token": "${API_DLT_TOKEN}"
}
END
./manage.py dlt_register_user "${DATASET_FILE}"
}
# wait until idhub api is prepared to received requests
wait_idhub() {
echo "Start waiting idhub API"
while true; do
result="$(curl -s "${url}" \
| jq -r .error \
|| echo "Reported errors, idhub API is still not ready")"
if [ "${result}" = "Invalid request method" ]; then
break
sleep 2
else
echo "Waiting idhub API"
sleep 3
fi
done
}
demo__send_to_sign_credential() {
filepath="${1}"
# hashlib.sha3_256 of PREDEFINED_TOKEN for idhub
DEMO_IDHUB_PREDEFINED_TOKEN="${DEMO_IDHUB_PREDEFINED_TOKEN:-}"
auth_header="Authorization: Bearer ${DEMO_IDHUB_PREDEFINED_TOKEN}"
json_header='Content-Type: application/json'
curl -s -X POST \
-H "${json_header}" \
-H "${auth_header}" \
-d @"${filepath}" \
"${url}" \
| jq -r .data
}
run_demo() {
if [ "${DEMO_IDHUB_DOMAIN:-}" ]; then
DEMO_IDHUB_DOMAIN="${DEMO_IDHUB_DOMAIN:-}"
# this demo only works with FQDN domain (with no ports)
url="https://${DEMO_IDHUB_DOMAIN}/webhook/sign/"
wait_idhub
demo__send_to_sign_credential \
'example/demo-snapshots-vc/snapshot_pre-verifiable-credential.json' \
> 'example/snapshots/snapshot_workbench-script_verifiable-credential.json'
fi
/usr/bin/time ./manage.py up_snapshots example/snapshots/ "${INIT_USER}"
}
config_phase() {
# TODO review this flag file
init_flagfile="${program_dir}/already_configured"
if [ ! -f "${init_flagfile}" ]; then
# non DL user (only for the inventory)
./manage.py add_institution "${INIT_ORG}"
# TODO: one error on add_user, and you don't add user anymore
./manage.py add_user "${INIT_ORG}" "${INIT_USER}" "${INIT_PASSWD}" "${ADMIN}" "${PREDEFINED_TOKEN}"
if [ "${DPP:-}" = 'true' ]; then
# 12, 13, 14
config_dpp_part1
# cleanup other snapshots and copy dlt/dpp snapshots
# TODO make this better
rm example/snapshots/*
cp example/dpp-snapshots/*.json example/snapshots/
fi
# # 15. Add inventory snapshots for user "${INIT_USER}".
if [ "${DEMO:-}" = 'true' ]; then
run_demo
fi
# remain next command as the last operation for this if conditional
touch "${init_flagfile}"
fi
}
check_app_is_there() { check_app_is_there() {
if [ ! -f "./manage.py" ]; then if [ ! -f "./manage.py" ]; then
usage usage
@ -12,9 +187,11 @@ check_app_is_there() {
} }
deploy() { deploy() {
# TODO this is weird, find better workaround if [ -d /opt/devicehub-django/.git ]; then
git config --global --add safe.directory /opt/devicehub-django # TODO this is weird, find better workaround
export COMMIT=$(git log --format="%H %ad" --date=iso -n 1) git config --global --add safe.directory "${program_dir}"
export COMMIT=$(git log --format="%H %ad" --date=iso -n 1)
fi
if [ "${DEBUG:-}" = 'true' ]; then if [ "${DEBUG:-}" = 'true' ]; then
./manage.py print_settings ./manage.py print_settings
@ -30,19 +207,11 @@ deploy() {
# move the migrate thing in docker entrypoint # move the migrate thing in docker entrypoint
# inspired by https://medium.com/analytics-vidhya/django-with-docker-and-docker-compose-python-part-2-8415976470cc # inspired by https://medium.com/analytics-vidhya/django-with-docker-and-docker-compose-python-part-2-8415976470cc
echo "INFO detected NEW deployment" echo "INFO detected NEW deployment"
./manage.py migrate if [ ! -d "${program_dir}/db/" ]; then
INIT_ORG="${INIT_ORG:-example-org}" mkdir -p "${program_dir}/db/"
INIT_USER="${INIT_USER:-user@example.org}"
INIT_PASSWD="${INIT_PASSWD:-1234}"
ADMIN='True'
PREDEFINED_TOKEN="${PREDEFINED_TOKEN:-}"
./manage.py add_institution "${INIT_ORG}"
# TODO: one error on add_user, and you don't add user anymore
./manage.py add_user "${INIT_ORG}" "${INIT_USER}" "${INIT_PASSWD}" "${ADMIN}" "${PREDEFINED_TOKEN}"
if [ "${DEMO:-}" = 'true' ]; then
./manage.py up_snapshots example/snapshots/ "${INIT_USER}"
fi fi
./manage.py migrate
config_phase
fi fi
} }
@ -70,6 +239,7 @@ runserver() {
main() { main() {
program_dir='/opt/devicehub-django' program_dir='/opt/devicehub-django'
cd "${program_dir}" cd "${program_dir}"
gen_env_vars
deploy deploy
runserver runserver
} }

View file

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

166
dpp/api_dlt.py Normal file
View file

@ -0,0 +1,166 @@
import json
import time
import logging
from django.conf import settings
from ereuseapi.methods import API
from dpp.models import Proof, UserDpp
logger = logging.getLogger('django')
# """The code of the status response of api dlt."""
STATUS_CODE = {
"Success": 201,
"Notwork": 400
}
ALGORITHM = "sha3-256"
PROOF_TYPE = {
'Register': 'Register',
'IssueDPP': 'IssueDPP',
'proof_of_recycling': 'proof_of_recycling',
'Erase': 'Erase',
'EWaste': 'EWaste',
}
def connect_api(user):
dp = UserDpp.objects.filter(user=user).first()
if not dp:
return
api_dlt = settings.API_DLT
token_dlt = dp.api_keys_dlt
if not api_dlt or not token_dlt:
logger.error("NOT POSSIBLE CONNECT WITH API DLT!!!")
return
return API(api_dlt, token_dlt, "ethereum")
def register_dlt(api, chid, phid, proof_type=None):
if proof_type:
return api.generate_proof(
chid,
ALGORITHM,
phid,
proof_type,
settings.ID_FEDERATED
)
return api.register_device(
chid,
ALGORITHM,
phid,
settings.ID_FEDERATED
)
def issuer_dpp_dlt(api, dpp):
phid = dpp.split(":")[1]
return api.issue_passport(
dpp,
ALGORITHM,
phid,
settings.ID_FEDERATED
)
def save_proof(signature, ev_uuid, result, proof_type, user):
if result['Status'] == STATUS_CODE.get("Success"):
timestamp = result.get('Data', {}).get('data', {}).get('timestamp')
if not timestamp:
return
logger.debug("timestamp: %s", timestamp)
d = {
"type": proof_type,
"timestamp": timestamp,
"issuer": user.institution,
"user": user,
"uuid": ev_uuid,
"signature": signature,
}
Proof.objects.create(**d)
def register_device_dlt(chid, phid, ev_uuid, user):
cny_a = 1
while cny_a:
api = connect_api(user)
if not api:
cny_a = 0
return
result = register_dlt(api, chid, phid)
try:
assert result['Status'] == STATUS_CODE.get("Success")
assert result['Data']['data']['timestamp']
cny_a = 0
except Exception:
if result.get("Data") != "Device already exists":
logger.error("API return: %s", result)
time.sleep(10)
else:
cny_a = 0
save_proof(phid, ev_uuid, result, PROOF_TYPE['Register'], user)
# TODO is neccesary?
if settings.ID_FEDERATED:
cny = 1
while cny:
try:
api.add_service(
chid,
'DeviceHub',
settings.ID_FEDERATED,
'Inventory service',
'Inv',
)
cny = 0
except Exception:
time.sleep(10)
def register_passport_dlt(chid, phid, ev_uuid, user):
token_dlt = settings.TOKEN_DLT
api_dlt = settings.API_DLT
if not token_dlt or not api_dlt:
return
dpp = "{chid}:{phid}".format(chid=chid, phid=phid)
if Proof.objects.filter(signature=dpp, type=PROOF_TYPE['IssueDPP']).exists():
return
cny_a = 1
while cny_a:
try:
api = connect_api(user)
if not api:
cny_a = 0
return
result = issuer_dpp_dlt(api, dpp)
cny_a = 0
except Exception as err:
logger.error("ERROR API issue passport return: %s", err)
time.sleep(10)
if result['Status'] is not STATUS_CODE.get("Success"):
logger.error("ERROR API issue passport return: %s", result)
return
save_proof(phid, ev_uuid, result, PROOF_TYPE['IssueDPP'], user)

View file

@ -1,6 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
class DocumentsConfig(AppConfig): class DppConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "documents" name = "dpp"

View file

@ -0,0 +1,35 @@
import logging
import requests
from django.core.management.base import BaseCommand
from django.conf import settings
from user.models import Institution
logger = logging.getLogger('django')
class Command(BaseCommand):
help = "Insert a new Institution in DLT"
def add_arguments(self, parser):
parser.add_argument('domain', type=str, help='institution')
def handle(self, *args, **kwargs):
domain = kwargs.get("domain")
api = settings.API_RESOLVER
if not api:
logger.error("you need set the var API_RESOLVER")
return
if "http" not in domain:
logger.error("you need put https:// in %s", domain)
return
api = api.strip("/")
domain = domain.strip("/")
data = {"url": domain}
url = api + '/registerURL'
res = requests.post(url, json=data)
print(res.json())

View file

@ -0,0 +1,72 @@
import json
import logging
from ereuseapi.methods import API
from django.conf import settings
from django.core.management.base import BaseCommand
from user.models import User, Institution
from dpp.models import UserDpp
logger = logging.getLogger('django')
class Command(BaseCommand):
help = "Insert users than are in Dlt with params: path of data set file"
def add_arguments(self, parser):
parser.add_argument('dataset_file', type=str, help='institution')
def handle(self, *args, **kwargs):
dataset_file = kwargs.get("dataset_file")
self.api_dlt = settings.API_DLT
self.institution = Institution.objects.filter().first()
if not self.api_dlt:
logger.error("you need set the var API_DLT")
return
self.api_dlt = self.api_dlt.strip("/")
with open(dataset_file) as f:
dataset = json.loads(f.read())
self.add_user(dataset)
def add_user(self, data):
email = data.get("email")
password = data.get("password")
api_token = data.get("api_token")
# ethereum = {"data": {"api_token": api_token}}
# data_eth = json.dumps(ethereum)
data_eth = json.dumps(api_token)
# TODO encrypt in the future
# api_keys_dlt = encrypt(password, data_eth)
api_keys_dlt = data_eth.strip('"').strip("'")
user = User.objects.filter(email=email).first()
if not user:
user = User.objects.create(
email=email,
password=password,
institution = self.institution
)
roles = []
token_dlt = api_token
api = API(self.api_dlt, token_dlt, "ethereum")
result = api.check_user_roles()
if result.get('Status') == 200:
if 'Success' in result.get('Data', {}).get('status'):
rols = result.get('Data', {}).get('data', {})
roles = [(k, k) for k, v in rols.items() if v]
roles_dlt = json.dumps(roles)
UserDpp.objects.create(
roles_dlt=roles_dlt,
api_keys_dlt=api_keys_dlt,
user=user
)

View file

@ -0,0 +1,47 @@
import logging
import requests
from django.core.management.base import BaseCommand
from django.conf import settings
from dpp.models import MemberFederated
logger = logging.getLogger('django')
class Command(BaseCommand):
help = "Synchronize members of DLT"
def handle(self, *args, **kwargs):
api = settings.API_RESOLVER
if not api:
logger.error("you need set the var API_RESOLVER")
return
api = api.strip("/")
url = api + '/getAll'
res = requests.get(url)
if res.status_code != 200:
return "Error, {}".format(res.text)
response = res.json()
members = response['url']
counter = members.pop('counter')
if counter <= MemberFederated.objects.count():
logger.info("Synchronize members of DLT -> All Ok")
return "All ok"
for k, v in members.items():
id = self.clean_id(k)
member = MemberFederated.objects.filter(dlt_id_provider=id).first()
if member:
if member.domain != v:
member.domain = v
member.save()
continue
MemberFederated.objects.create(dlt_id_provider=id, domain=v)
return res.text
def clean_id(self, id):
return int(id.split('DH')[-1])

View file

@ -0,0 +1,52 @@
# Generated by Django 5.0.6 on 2024-11-18 14:29
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="Proof",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("timestamp", models.IntegerField()),
("uuid", models.UUIDField()),
("signature", models.CharField(max_length=256)),
("type", models.CharField(max_length=256)),
(
"issuer",
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,
),
),
],
),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 5.0.6 on 2024-11-19 19:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("dpp", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="MemberFederated",
fields=[
(
"dlt_id_provider",
models.IntegerField(primary_key=True, serialize=False),
),
("domain", models.CharField(max_length=256)),
("client_id", models.CharField(max_length=256)),
("client_secret", models.CharField(max_length=256)),
],
),
]

View file

@ -0,0 +1,60 @@
# Generated by Django 5.0.6 on 2024-11-20 10:51
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("dpp", "0002_memberfederated"),
("user", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="memberfederated",
name="institution",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="user.institution",
),
),
migrations.AlterField(
model_name="memberfederated",
name="client_id",
field=models.CharField(max_length=256, null=True),
),
migrations.AlterField(
model_name="memberfederated",
name="client_secret",
field=models.CharField(max_length=256, null=True),
),
migrations.CreateModel(
name="UserDpp",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("roles_dlt", models.TextField()),
("api_keys_dlt", models.TextField()),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

32
dpp/models.py Normal file
View file

@ -0,0 +1,32 @@
from django.db import models
from user.models import User, Institution
from utils.constants import STR_EXTEND_SIZE
# Create your models here.
class Proof(models.Model):
## The signature can be a phid or dpp depending of type of Proof
timestamp = models.IntegerField()
uuid = models.UUIDField()
signature = models.CharField(max_length=STR_EXTEND_SIZE)
type = models.CharField(max_length=STR_EXTEND_SIZE)
issuer = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, blank=True)
class MemberFederated(models.Model):
dlt_id_provider = models.IntegerField(primary_key=True)
domain = models.CharField(max_length=STR_EXTEND_SIZE)
# This client_id and client_secret is used for connected to this domain as
# a client and this domain then is the server of auth
client_id = models.CharField(max_length=STR_EXTEND_SIZE, null=True)
client_secret = models.CharField(max_length=STR_EXTEND_SIZE, null=True)
institution = models.ForeignKey(
Institution, on_delete=models.SET_NULL, null=True, blank=True)
class UserDpp(models.Model):
roles_dlt = models.TextField()
api_keys_dlt = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)

8
dpp/urls.py Normal file
View file

@ -0,0 +1,8 @@
from django.urls import path
from dpp import views
app_name = 'dpp'
urlpatterns = [
path("<int:proof_id>/", views.ProofView.as_view(), name="proof"),
]

40
dpp/views.py Normal file
View file

@ -0,0 +1,40 @@
import json
import logging
import hashlib
from django.views.generic.edit import View
from django.http import JsonResponse
from dpp.api_dlt import ALGORITHM
from evidence.models import Evidence
from evidence.parse import Build
from dpp.models import Proof
class ProofView(View):
def get(self, request, *args, **kwargs):
timestamp = kwargs.get("proof_id")
proof = Proof.objects.filter(timestamp=timestamp).first()
if not proof:
return JsonResponse({}, status=404)
ev = Evidence(proof.uuid)
if not ev.doc:
return JsonResponse({}, status=404)
dev = Build(ev.doc, None, check=True)
doc = dev.build.get_doc()
data = {
"algorithm": ALGORITHM,
"document": json.dumps(doc)
}
d = {
'@context': ['https://ereuse.org/proof0.json'],
'data': data,
}
response = JsonResponse(d, status=200)
response["Access-Control-Allow-Origin"] = "*"
return response

View file

@ -4,12 +4,13 @@ import pandas as pd
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from utils.device import create_annotation, create_doc, create_index from utils.device import create_property, create_doc, create_index
from utils.forms import MultipleFileField from utils.forms import MultipleFileField
from device.models import Device from device.models import Device
from evidence.parse import Build from evidence.parse import Build
from evidence.models import Annotation from evidence.models import SystemProperty, UserProperty
from utils.save_snapshots import move_json, save_in_disk from utils.save_snapshots import move_json, save_in_disk
from action.models import DeviceLog
class UploadForm(forms.Form): class UploadForm(forms.Form):
@ -29,17 +30,17 @@ class UploadForm(forms.Form):
try: try:
file_json = json.loads(file_data) file_json = json.loads(file_data)
Build(file_json, None, check=True) snap = Build(file_json, None, check=True)
exist_annotation = Annotation.objects.filter( exists_property = SystemProperty.objects.filter(
uuid=file_json['uuid'] uuid=snap.uuid
).first() ).first()
if exist_annotation: if exists_property:
raise ValidationError( raise ValidationError(
_("The snapshot already exists"), _("The snapshot already exists"),
code="duplicate_snapshot", code="duplicate_snapshot",
) )
#Catch any error and display it as Validation Error so the Form handles it #Catch any error and display it as Validation Error so the Form handles it
except Exception as e: except Exception as e:
raise ValidationError( raise ValidationError(
@ -57,7 +58,9 @@ class UploadForm(forms.Form):
for ev in self.evidences: for ev in self.evidences:
path_name = save_in_disk(ev[1], user.institution.name) 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) move_json(path_name, user.institution.name)
@ -68,9 +71,8 @@ class UserTagForm(forms.Form):
self.pk = None self.pk = None
self.uuid = kwargs.pop('uuid', None) self.uuid = kwargs.pop('uuid', None)
self.user = kwargs.pop('user') self.user = kwargs.pop('user')
instance = Annotation.objects.filter( instance = SystemProperty.objects.filter(
uuid=self.uuid, uuid=self.uuid,
type=Annotation.Type.SYSTEM,
key='CUSTOM_ID', key='CUSTOM_ID',
owner=self.user.institution owner=self.user.institution
).first() ).first()
@ -86,9 +88,8 @@ class UserTagForm(forms.Form):
if not data: if not data:
return False return False
self.tag = data self.tag = data
self.instance = Annotation.objects.filter( self.instance = SystemProperty.objects.filter(
uuid=self.uuid, uuid=self.uuid,
type=Annotation.Type.SYSTEM,
key='CUSTOM_ID', key='CUSTOM_ID',
owner=self.user.institution owner=self.user.institution
).first() ).first()
@ -100,20 +101,31 @@ class UserTagForm(forms.Form):
return return
if self.instance: if self.instance:
old_value = self.instance.value
if not self.tag: if not self.tag:
message =_("<Deleted> Evidence Tag. Old Value: '{}'").format(old_value)
self.instance.delete() self.instance.delete()
self.instance.value = self.tag else:
self.instance.save() self.instance.value = self.tag
return self.instance.save()
if old_value != self.tag:
Annotation.objects.create( message=_("<Updated> Evidence Tag. Old Value: '{}'. New Value: '{}'").format(old_value, self.tag)
uuid=self.uuid, else:
type=Annotation.Type.SYSTEM, message =_("<Created> Evidence Tag. Value: '{}'").format(self.tag)
key='CUSTOM_ID', SystemProperty.objects.create(
value=self.tag, uuid=self.uuid,
owner=self.user.institution, key='CUSTOM_ID',
user=self.user 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
)
class ImportForm(forms.Form): class ImportForm(forms.Form):
@ -164,8 +176,8 @@ class ImportForm(forms.Form):
table = [] table = []
for row in self.rows: for row in self.rows:
doc = create_doc(row) doc = create_doc(row)
annotation = create_annotation(doc, self.user) property = create_property(doc, self.user)
table.append((doc, annotation)) table.append((doc, property))
if commit: if commit:
for doc, cred in table: for doc, cred in table:
@ -186,9 +198,9 @@ class EraseServerForm(forms.Form):
self.pk = None self.pk = None
self.uuid = kwargs.pop('uuid', None) self.uuid = kwargs.pop('uuid', None)
self.user = kwargs.pop('user') self.user = kwargs.pop('user')
instance = Annotation.objects.filter( instance = UserProperty.objects.filter(
uuid=self.uuid, uuid=self.uuid,
type=Annotation.Type.ERASE_SERVER, type=UserProperty.Type.ERASE_SERVER,
key='ERASE_SERVER', key='ERASE_SERVER',
owner=self.user.institution owner=self.user.institution
).first() ).first()
@ -201,9 +213,9 @@ class EraseServerForm(forms.Form):
def clean(self): def clean(self):
self.erase_server = self.cleaned_data.get('erase_server', False) self.erase_server = self.cleaned_data.get('erase_server', False)
self.instance = Annotation.objects.filter( self.instance = UserProperty.objects.filter(
uuid=self.uuid, uuid=self.uuid,
type=Annotation.Type.ERASE_SERVER, type=UserProperty.Type.ERASE_SERVER,
key='ERASE_SERVER', key='ERASE_SERVER',
owner=self.user.institution owner=self.user.institution
).first() ).first()
@ -221,10 +233,10 @@ class EraseServerForm(forms.Form):
if self.instance: if self.instance:
return return
Annotation.objects.create( UserProperty.objects.create(
uuid=self.uuid, uuid=self.uuid,
type=Annotation.Type.ERASE_SERVER, type=UserProperty.Type.ERASE_SERVER,
key='ERASE_SERVER', key='ERASE_SERVER',
value=self.erase_server, value=self.erase_server,
owner=self.user.institution, owner=self.user.institution,

74
evidence/legacy_parse.py Normal file
View file

@ -0,0 +1,74 @@
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.type = 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.device = data.device
self.components = data.components
self.device.pop("actions", None)
for c in self.components:
c.pop("actions", None)

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

View file

@ -4,7 +4,6 @@ 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 utils.save_snapshots import move_json, save_in_disk
from evidence.parse import Build from evidence.parse import Build

View file

@ -0,0 +1,107 @@
# 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

@ -0,0 +1,17 @@
# 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",
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 5.0.6 on 2025-01-29 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("evidence", "0004_remove_userproperty_user_unique_type_key_uuid"),
]
operations = [
migrations.AlterField(
model_name="userproperty",
name="type",
field=models.SmallIntegerField(
choices=[(1, "User"), (2, "EraseServer")], default=1
),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 5.0.6 on 2025-01-30 17:52
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('evidence', '0005_alter_userproperty_type'),
('user', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddConstraint(
model_name='userproperty',
constraint=models.UniqueConstraint(fields=('key', 'uuid'), name='userproperty_unique_type_key_uuid'),
),
]

60
evidence/mixin_parse.py Normal file
View file

@ -0,0 +1,60 @@
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.type = ""
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 = {}
for k in ALGOS.keys():
if not settings.DPP and k == 'ereuse22':
continue
self.algorithms[k] = self.get_hid(k)
def get_doc(self):
self._get_components()
components = sorted(self.components, key=lambda x: x.get("type"))
device = self.algorithms.get('ereuse22')
doc = [("computer", device)]
for c in components:
doc.append((c.get("type"), self.get_id_hw_dpp(c)))
return doc
def get_id_hw_dpp(self, d):
algorithm = ALGOS.get("ereuse22", [])
hid = ""
for f in algorithm:
hid += d.get(f, '')
return hid

View file

@ -1,34 +1,54 @@
import json import json
import hashlib
from dmidecode import DMIParse from dmidecode import DMIParse
from django.db import models from django.db import models
from django.db.models import Q
from utils.constants import STR_EXTEND_SIZE, CHASSIS_DH from utils.constants import 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 evidence.normal_parse_details import get_inxi, get_inxi_key
from user.models import User, Institution from user.models import User, Institution
class Annotation(models.Model): class Property(models.Model):
class Type(models.IntegerChoices):
SYSTEM = 0, "System"
USER = 1, "User"
DOCUMENT = 2, "Document"
ERASE_SERVER = 3, "EraseServer"
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
uuid = models.UUIDField()
owner = models.ForeignKey(Institution, on_delete=models.CASCADE) owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey( user = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, blank=True) User, on_delete=models.SET_NULL, null=True, blank=True)
type = models.SmallIntegerField(choices=Type)
key = models.CharField(max_length=STR_EXTEND_SIZE) key = models.CharField(max_length=STR_EXTEND_SIZE)
value = 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: class Meta:
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
fields=["type", "key", "uuid"], name="unique_type_key_uuid") fields=["key", "uuid"], name="system_unique_type_key_uuid")
]
class UserProperty(Property):
class Type(models.IntegerChoices):
USER = 1, "User"
ERASE_SERVER = 2, "EraseServer"
uuid = models.UUIDField()
type = models.SmallIntegerField(choices=Type, default=Type.USER)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["key", "uuid"], name="userproperty_unique_type_key_uuid")
] ]
@ -39,29 +59,39 @@ class Evidence:
self.doc = None self.doc = None
self.created = None self.created = None
self.dmi = None self.dmi = None
self.annotations = [] self.inxi = None
self.properties = []
self.components = [] self.components = []
self.default = "n/a" self.default = "n/a"
self.get_owner() self.get_owner()
self.get_time() self.get_time()
def get_annotations(self): def get_properties(self):
self.annotations = Annotation.objects.filter( self.properties = SystemProperty.objects.filter(
uuid=self.uuid uuid=self.uuid
).order_by("created") ).order_by("created")
def get_owner(self): def get_owner(self):
if not self.annotations: if not self.properties:
self.get_annotations() self.get_properties()
a = self.annotations.first() a = self.properties.first()
if a: if a:
self.owner = a.owner self.owner = a.owner
def get_phid(self):
if not self.doc:
self.get_doc()
return hashlib.sha3_256(json.dumps(self.doc)).hexdigest()
def get_doc(self): def get_doc(self):
self.doc = {} self.doc = {}
self.inxi = None
if not self.owner: if not self.owner:
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 and matches.size() < 0:
@ -70,9 +100,36 @@ class Evidence:
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.is_legacy():
return
if self.doc.get("credentialSubject"):
for ev in self.doc["evidence"]:
if "dmidecode" == ev.get("operation"):
dmidecode_raw = ev["output"]
if "inxi" == ev.get("operation"):
self.inxi = ev["output"]
else:
dmidecode_raw = self.doc["data"]["dmidecode"] dmidecode_raw = self.doc["data"]["dmidecode"]
inxi_raw = self.doc.get("data", {}).get("inxi")
self.dmi = DMIParse(dmidecode_raw) self.dmi = DMIParse(dmidecode_raw)
try:
self.inxi = json.loads(inxi_raw)
except Exception:
pass
if self.inxi:
try:
machine = get_inxi_key(self.inxi, 'Machine')
for m in machine:
system = get_inxi(m, "System")
if system:
self.device_manufacturer = system
self.device_model = get_inxi(m, "product")
self.device_serial_number = get_inxi(m, "serial")
self.device_chassis = get_inxi(m, "Type")
self.device_version = get_inxi(m, "v")
except Exception:
return
def get_time(self): def get_time(self):
if not self.doc: if not self.doc:
@ -80,7 +137,7 @@ class Evidence:
self.created = self.doc.get("endTime") self.created = self.doc.get("endTime")
if not self.created: if not self.created:
self.created = self.annotations.last().created self.created = self.properties.last().created
def get_components(self): def get_components(self):
if self.is_legacy(): if self.is_legacy():
@ -96,7 +153,10 @@ class Evidence:
return list(self.doc.get('kv').values())[0] return list(self.doc.get('kv').values())[0]
if self.is_legacy(): if self.is_legacy():
return self.doc['device']['manufacturer'] return self.doc.get('device', {}).get('manufacturer', '')
if self.inxi:
return self.device_manufacturer
return self.dmi.manufacturer().strip() return self.dmi.manufacturer().strip()
@ -108,13 +168,19 @@ class Evidence:
return list(self.doc.get('kv').values())[1] return list(self.doc.get('kv').values())[1]
if self.is_legacy(): if self.is_legacy():
return self.doc['device']['model'] return self.doc.get('device', {}).get('model', '')
if self.inxi:
return self.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.is_legacy():
return self.doc['device']['model'] return self.doc.get('device', {}).get('model', '')
if self.inxi:
return self.device_chassis
chassis = self.dmi.get("Chassis")[0].get("Type", '_virtual') chassis = self.dmi.get("Chassis")[0].get("Type", '_virtual')
lower_type = chassis.lower() lower_type = chassis.lower()
@ -126,23 +192,45 @@ class Evidence:
def get_serial_number(self): def get_serial_number(self):
if self.is_legacy(): if self.is_legacy():
return self.doc['device']['serialNumber'] return self.doc.get('device', {}).get('serialNumber', '')
if self.inxi:
return self.device_serial_number
return self.dmi.serial_number().strip() return self.dmi.serial_number().strip()
def get_version(self):
if self.inxi:
return self.device_version
return ""
@classmethod @classmethod
def get_all(cls, user): def get_all(cls, user):
return Annotation.objects.filter( return SystemProperty.objects.filter(
owner=user.institution, owner=user.institution,
type=Annotation.Type.SYSTEM, key="ereuse24",
key="hidalgo1",
).order_by("-created").values_list("uuid", "created").distinct() ).order_by("-created").values_list("uuid", "created").distinct()
def set_components(self): def set_components(self):
snapshot = ParseSnapshot(self.doc).snapshot_json self.components = ParseSnapshot(self.doc).components
self.components = snapshot['components']
def is_legacy(self): def is_legacy(self):
if self.doc.get("credentialSubject"):
return False
return self.doc.get("software") != "workbench-script" return self.doc.get("software") != "workbench-script"
def is_web_snapshot(self): def is_web_snapshot(self):
return self.doc.get("type") == "WebSnapshot" return self.doc.get("type") == "WebSnapshot"
def did_document(self):
if not self.doc.get("credentialSubject"):
return ''
did = self.doc.get('issuer')
if not "did:web" in did:
return ''
return "https://{}/did.json".format(
did.split("did:web:")[1].replace(":", "/")
)

71
evidence/normal_parse.py Normal file
View file

@ -0,0 +1,71 @@
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.type = get_inxi(m, "Type")
self.chassis = self.type
self.version = get_inxi(m, "v")
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.device = data.device
self.components = data.components
self.device.pop("actions", None)
for c in self.components:
c.pop("actions", None)

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)

28
evidence/old_parse.py Normal file
View file

@ -0,0 +1,28 @@
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):
self.device = self.json.get('device', {})
self.manufacturer = self.device.get("manufacturer", '')
self.model = self.device.get("model", '')
self.chassis = self.device.get("chassis", '')
self.serial_number = self.device.get("serialNumber", '')
self.sku = self.device.get("sku", '')
self.type = self.device.get("type", '')
self.version = self.device.get("version", '')
def _get_components(self):
self.components = self.json.get("components", [])
self.device.pop("actions", None)
for c in self.components:
c.pop("actions", None)

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,130 +2,94 @@ import json
import hashlib import hashlib
import logging import logging
from dmidecode import DMIParse from evidence import legacy_parse
from json_repair import repair_json from evidence import old_parse
from evidence.parse_details import get_lshw_child from evidence import normal_parse
from evidence.parse_details import ParseSnapshot
from evidence.models import Annotation from evidence.models import SystemProperty
from evidence.xapian import index from evidence.xapian import index
from utils.constants import CHASSIS_DH from evidence.normal_parse_details import get_inxi_key, get_inxi
from django.conf import settings
if settings.DPP:
from dpp.api_dlt import register_device_dlt, register_passport_dlt
logger = logging.getLogger('django') 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
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: class Build:
def __init__(self, evidence_json, user, check=False): def __init__(self, evidence_json, user, check=False):
self.json = evidence_json """
self.uuid = self.json['uuid'] 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 self.user = user
self.hid = None
self.generate_chids() 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: if check:
return return
self.index() self.index()
self.create_annotations() self.create_annotations()
if settings.DPP:
self.register_device_dlt()
def index(self): def index(self):
snap = json.dumps(self.json) snap = json.dumps(self.evidence)
index(self.user.institution, self.uuid, snap) index(self.user.institution, self.uuid, snap)
def generate_chids(self):
self.algorithms = {
'hidalgo1': self.get_hid_14(),
}
def get_hid_14(self):
if self.json.get("software") == "workbench-script":
hid = self.get_hid(self.json)
else:
device = self.json['device']
manufacturer = device.get("manufacturer", '')
model = device.get("model", '')
chassis = device.get("chassis", '')
serial_number = device.get("serialNumber", '')
sku = device.get("sku", '')
hid = f"{manufacturer}{model}{chassis}{serial_number}{sku}"
return hashlib.sha3_256(hid.encode()).hexdigest()
def create_annotations(self): def create_annotations(self):
annotation = Annotation.objects.filter( prop = SystemProperty.objects.filter(
uuid=self.uuid, uuid=self.uuid,
owner=self.user.institution, owner=self.user.institution,
type=Annotation.Type.SYSTEM,
) )
if annotation: if prop:
txt = "Warning: Snapshot %s already registered (annotation exists)" txt = "Warning: Snapshot %s already registered (annotation exists)"
logger.warning(txt, self.uuid) logger.warning(txt, self.uuid)
return return
for k, v in self.algorithms.items(): for k, v in self.build.algorithms.items():
Annotation.objects.create( SystemProperty.objects.create(
uuid=self.uuid, uuid=self.uuid,
owner=self.user.institution, owner=self.user.institution,
user=self.user, user=self.user,
type=Annotation.Type.SYSTEM,
key=k, key=k,
value=v value=self.sign(v)
) )
def get_chassis_dh(self): def sign(self, doc):
chassis = self.get_chassis() return hashlib.sha3_256(doc.encode()).hexdigest()
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): def register_device_dlt(self):
return self.dmi.get("System")[0].get("SKU Number", "n/a").strip() legacy_dpp = self.build.algorithms.get('ereuse22')
chid = self.sign(legacy_dpp)
def get_chassis(self): phid = self.sign(json.dumps(self.build.get_doc()))
return self.dmi.get("Chassis")[0].get("Type", '_virtual') register_device_dlt(chid, phid, self.uuid, self.user)
register_passport_dlt(chid, phid, self.uuid, self.user)
def get_hid(self, snapshot):
dmidecode_raw = snapshot["data"]["dmidecode"]
self.dmi = DMIParse(dmidecode_raw)
manufacturer = self.dmi.manufacturer().strip()
model = self.dmi.model().strip()
chassis = self.get_chassis_dh()
serial_number = self.dmi.serial_number()
sku = self.get_sku()
if not snapshot["data"].get('lshw'):
return f"{manufacturer}{model}{chassis}{serial_number}{sku}"
lshw = snapshot["data"]["lshw"]
# mac = get_mac2(hwinfo_raw) or ""
mac = get_mac(lshw) or ""
if not mac:
txt = "Could not retrieve MAC address in snapshot %s"
logger.warning(txt, snapshot['uuid'])
return f"{manufacturer}{model}{chassis}{serial_number}{sku}{mac}"

View file

@ -1,505 +1,38 @@
import json
import logging import logging
import numpy as np
from datetime import datetime from evidence import (
from dmidecode import DMIParse legacy_parse_details,
from json_repair import repair_json normal_parse_details,
old_parse_details
from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE )
logger = logging.getLogger('django') logger = logging.getLogger('django')
def get_lshw_child(child, nets, component):
if child.get('id') == component:
nets.append(child)
if child.get('children'):
[get_lshw_child(x, nets, component) for x in child['children']]
class ParseSnapshot: class ParseSnapshot:
def __init__(self, snapshot, default="n/a"): def __init__(self, snapshot, default="n/a"):
self.default = default if snapshot.get("credentialSubject"):
self.dmidecode_raw = snapshot["data"].get("dmidecode", "{}") self.build = normal_parse_details.ParseSnapshot(
self.smart_raw = snapshot["data"].get("disks", []) snapshot,
self.hwinfo_raw = snapshot["data"].get("hwinfo", "") default=default
self.lshw_raw = snapshot["data"].get("lshw", {}) or {} )
self.lscpi_raw = snapshot["data"].get("lspci", "") elif snapshot.get("software") != "workbench-script":
self.device = {"actions": []} self.build = old_parse_details.ParseSnapshot(
self.components = [] snapshot,
self.monitors = [] default=default
)
self.dmi = DMIParse(self.dmidecode_raw) elif snapshot.get("data",{}).get("lshw"):
self.smart = self.loads(self.smart_raw) self.build = legacy_parse_details.ParseSnapshot(
self.lshw = self.loads(self.lshw_raw) snapshot,
self.hwinfo = self.parse_hwinfo() default=default
)
self.set_computer() else:
self.get_hwinfo_monitors() self.build = normal_parse_details.ParseSnapshot(
self.set_components() snapshot,
self.snapshot_json = { default=default
"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": self.get_memory_video(c),
"manufacturer": c.get("vendor", self.default),
"model": c.get("product", self.default),
"serialNumber": c.get("serial", self.default),
}
)
def get_memory_video(self, c):
# get info of lspci
# pci_id = c['businfo'].split('@')[1]
# lspci.get(pci_id) | grep size
# lspci -v -s 00:02.0
return None
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:
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
for ch in self.lshw.get('children', []):
for c in ch.get('children', []):
if c['class'] == 'processor':
return c.get('width', 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(err))
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)
self.default = default
self.device = self.build.snapshot_json.get("device")
self.components = self.build.snapshot_json.get("components")

View file

@ -1,43 +0,0 @@
from rest_framework import serializers
from evidence.models import EvidenceJson
import json
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
from evidence.parse import Parse
class EvidenceSerializer(serializers.ModelSerializer):
class Meta:
model = EvidenceJson
fields = ['id', 'title', 'content']
@csrf_exempt
def webhook_verify(request):
if request.method == 'POST':
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return JsonResponse({'error': 'Invalid authorization'}, status=401)
token = auth_header.split(' ')[1]
tk = Token.objects.filter(token=token).first()
if not tk:
return JsonResponse({'error': 'Invalid authorization'}, status=401)
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
try:
device = Parse(data)
except Exception:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
if not device:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
return JsonResponse({"result": "Ok"}, status=200)
return JsonResponse({'error': 'Invalid request method'}, status=400)

View file

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

View file

@ -20,5 +20,5 @@ urlpatterns = [
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>/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("tag/<str:pk>/delete", views.DeleteEvidenceTagView.as_view(), name="delete_tag"),
] ]

View file

@ -12,8 +12,9 @@ from django.views.generic.edit import (
FormView, FormView,
) )
from action.models import DeviceLog
from dashboard.mixins import DashboardView, Http403 from dashboard.mixins import DashboardView, Http403
from evidence.models import Evidence, Annotation from evidence.models import SystemProperty, UserProperty, Evidence
from evidence.forms import ( from evidence.forms import (
UploadForm, UploadForm,
UserTagForm, UserTagForm,
@ -95,7 +96,7 @@ class EvidenceView(DashboardView, FormView):
if self.object.owner != self.request.user.institution: if self.object.owner != self.request.user.institution:
raise Http403 raise Http403
self.object.get_annotations() self.object.get_properties()
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -137,37 +138,10 @@ class DownloadEvidenceView(DashboardView, TemplateView):
evidence.get_doc() evidence.get_doc()
data = json.dumps(evidence.doc) data = json.dumps(evidence.doc)
response = HttpResponse(data, content_type="application/json") response = HttpResponse(data, content_type="application/json")
response['Content-Disposition'] = 'attachment; filename={}'.format("credential.json") response['Content-Disposition'] = 'attachment; filename={}'.format("evidence.json")
return response 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): class EraseServerView(DashboardView, FormView):
template_name = "ev_eraseserver.html" template_name = "ev_eraseserver.html"
section = "evidences" section = "evidences"
@ -182,7 +156,7 @@ class EraseServerView(DashboardView, FormView):
if self.object.owner != self.request.user.institution: if self.object.owner != self.request.user.institution:
raise Http403 raise Http403
self.object.get_annotations() self.object.get_properties()
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -211,3 +185,35 @@ class EraseServerView(DashboardView, FormView):
def get_success_url(self): def get_success_url(self):
success_url = reverse_lazy('evidence:details', args=[self.pk]) success_url = reverse_lazy('evidence:details', args=[self.pk])
return success_url 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])
)

View file

@ -22,10 +22,14 @@ def search(institution, qs, offset=0, limit=10):
qp.set_stemming_strategy(xapian.QueryParser.STEM_SOME) qp.set_stemming_strategy(xapian.QueryParser.STEM_SOME)
qp.add_prefix("uuid", "uuid") qp.add_prefix("uuid", "uuid")
query = qp.parse_query(qs) query = qp.parse_query(qs)
institution_term = "U{}".format(institution.id) if institution:
final_query = xapian.Query( institution_term = "U{}".format(institution.id)
xapian.Query.OP_AND, query, xapian.Query(institution_term) final_query = xapian.Query(
) xapian.Query.OP_AND, query, xapian.Query(institution_term)
)
else:
final_query = xapian.Query(query)
enquire = xapian.Enquire(database) enquire = xapian.Enquire(database)
enquire.set_query(final_query) enquire.set_query(final_query)
matches = enquire.get_mset(offset, limit) matches = enquire.get_mset(offset, limit)

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

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"closed": true, "components": [{"actions": [], "manufacturer": "Intel Corporation", "model": "82579LM Gigabit Network Connection", "serialNumber": "00:11:11:11:11:00", "speed": 1000.0, "type": "NetworkAdapter", "variant": "04", "wireless": false}, {"actions": [], "manufacturer": "Intel Corporation", "model": "7 Series/C216 Chipset Family High Definition Audio Controller", "serialNumber": null, "type": "SoundCard"}, {"actions": [], "format": "DIMM", "interface": "DDR3", "manufacturer": "Micron", "model": "16KTF51264AZ", "serialNumber": "AAAAAAAA", "size": 4096.0, "speed": 1600.0, "type": "RamModule"}, {"actions": [{"endTime": "2022-10-11T13:45:31.239555+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.623967+00:00", "steps": [{"endTime": "2021-10-11T11:05:28.090897+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.624163+00:00", "type": "StepZero"}, {"endTime": "2021-10-11T13:45:31.239402+00:00", "severity": "Info", "startTime": "2021-10-11T11:05:28.091255+00:00", "type": "StepRandom"}], "type": "EraseSectors"}, {"assessment": true, "commandTimeout": 30, "currentPendingSectorCount": 0, "elapsed": 60, "length": "Short", "lifetime": 18720, "offlineUncorrectable": 0, "powerCycleCount": 2147, "reallocatedSectorCount": 0, "reportedUncorrectableErrors": 0, "severity": "Info", "status": "Completed without error", "type": "TestDataStorage"}, {"elapsed": 11, "readSpeed": 119.0, "type": "BenchmarkDataStorage", "writeSpeed": 32.7}], "interface": "ATA", "manufacturer": "Seagate", "model": "ST3500418AS", "serialNumber": "AAAAAAAA", "size": 500000.0, "type": "HardDrive", "variant": "CC46"}, {"actions": [{"elapsed": 0, "rate": 25540.36, "type": "BenchmarkProcessor"}, {"elapsed": 8, "rate": 7.6939, "type": "BenchmarkProcessorSysbench"}], "address": 64, "brand": "Core i5", "cores": 4, "generation": 3, "manufacturer": "Intel Corp.", "model": "Intel Core i5-3470 CPU @ 3.20GHz", "serialNumber": null, "speed": 1.6242180000000002, "threads": 4, "type": "Processor"}, {"actions": [], "manufacturer": "Intel Corporation", "memory": null, "model": "Xeon E3-1200 v2/3rd Gen Core processor Graphics Controller", "serialNumber": null, "type": "GraphicCard"}, {"actions": [], "biosDate": "2012-08-07T00:00:00", "firewire": 0, "manufacturer": "LENOVO", "model": "MAHOBAY", "pcmcia": 0, "ramMaxSize": 32, "ramSlots": 4, "serial": 1, "serialNumber": null, "slots": 4, "type": "Motherboard", "usb": 3, "version": "9SKT39AUS"}], "device": {"actions": [{"elapsed": 1, "rate": 0.6507, "type": "BenchmarkRamSysbench"}], "chassis": "Tower", "manufacturer": "LENOVO", "model": "3227A2G", "serialNumber": "AAAAAAAA", "sku": "LENOVO_MT_3227", "type": "Desktop", "version": "ThinkCentre M92P"}, "elapsed": 187302510, "endTime": "2016-11-03T17:17:01.116554+00:00", "software": "Workbench", "type": "Snapshot", "uuid": "ae913de1-e639-476a-ad9b-78eabbe4628b", "version": "11.0b11"}

View file

@ -0,0 +1 @@
{"closed": true, "components": [{"actions": [], "manufacturer": "Intel Corporation", "model": "82579LM Gigabit Network Connection", "serialNumber": "00:11:11:11:11:00", "speed": 1000.0, "type": "NetworkAdapter", "variant": "04", "wireless": false}, {"actions": [], "manufacturer": "Intel Corporation", "model": "7 Series/C216 Chipset Family High Definition Audio Controller", "serialNumber": null, "type": "SoundCard"}, {"actions": [], "format": "DIMM", "interface": "DDR3", "manufacturer": "Micron", "model": "16KTF51264AZ", "serialNumber": "AAAAAAAA", "size": 4096.0, "speed": 1600.0, "type": "RamModule"}, {"actions": [{"endTime": "2022-10-11T13:45:31.239555+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.623967+00:00", "steps": [{"endTime": "2021-10-11T11:05:28.090897+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.624163+00:00", "type": "StepZero"}, {"endTime": "2021-10-11T13:45:31.239402+00:00", "severity": "Info", "startTime": "2021-10-11T11:05:28.091255+00:00", "type": "StepRandom"}], "type": "EraseSectors"}, {"assessment": true, "commandTimeout": 30, "currentPendingSectorCount": 0, "elapsed": 60, "length": "Short", "lifetime": 18720, "offlineUncorrectable": 0, "powerCycleCount": 2147, "reallocatedSectorCount": 0, "reportedUncorrectableErrors": 0, "severity": "Info", "status": "Completed without error", "type": "TestDataStorage"}, {"elapsed": 11, "readSpeed": 119.0, "type": "BenchmarkDataStorage", "writeSpeed": 32.7}], "interface": "ATA", "manufacturer": "Seagate", "model": "ST3500418AS", "serialNumber": "AAAAAAAA", "size": 500000.0, "type": "HardDrive", "variant": "CC46"}, {"actions": [{"elapsed": 0, "rate": 25540.36, "type": "BenchmarkProcessor"}, {"elapsed": 8, "rate": 7.6939, "type": "BenchmarkProcessorSysbench"}], "address": 64, "brand": "Core i5", "cores": 4, "generation": 3, "manufacturer": "Intel Corp.", "model": "Intel Core i5-3470 CPU @ 3.20GHz", "serialNumber": null, "speed": 1.6242180000000002, "threads": 4, "type": "Processor"}, {"actions": [], "manufacturer": "Intel Corporation", "memory": null, "model": "Xeon E3-1200 v2/3rd Gen Core processor Graphics Controller", "serialNumber": null, "type": "GraphicCard"}, {"actions": [], "biosDate": "2012-08-07T00:00:00", "firewire": 0, "manufacturer": "LENOVO", "model": "MAHOBAY", "pcmcia": 0, "ramMaxSize": 32, "ramSlots": 4, "serial": 1, "serialNumber": null, "slots": 4, "type": "Motherboard", "usb": 3, "version": "9SKT39AUS"}], "device": {"actions": [{"elapsed": 1, "rate": 0.6507, "type": "BenchmarkRamSysbench"}], "chassis": "Tower", "manufacturer": "LENOVO", "model": "3227A2G", "serialNumber": "AAAAAAAAD", "sku": "LENOVO_MT_3227", "type": "Desktop", "version": "ThinkCentre M92P"}, "elapsed": 187302510, "endTime": "2016-11-03T17:17:01.116554+00:00", "software": "Workbench", "type": "Snapshot", "uuid": "ae913de1-e639-476a-ad9b-78eabbe4625b", "version": "11.0b11"}

Some files were not shown because too many files have changed in this diff Show more