Compare commits
711 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8291fa554 | |||
| 3f7b4b43e2 | |||
| 0c9e8c1774 | |||
| b0d2506cb8 | |||
| f1b8cb3872 | |||
| 0f4e089556 | |||
| 599aa550b1 | |||
| 6f27570f25 | |||
| 8471a4df03 | |||
| 35d87dfe3c | |||
| e1fb33d668 | |||
| 489ff1184f | |||
| af05954590 | |||
| e9e02739f6 | |||
| 6d4cb2f2e0 | |||
| 2efb4485fd | |||
| 742c5373a3 | |||
| 797a30e30b | |||
| f15c2ff0da | |||
| 875342471d | |||
| 9b3c1e91fe | |||
| a2ce99d14b | |||
| 946ebe9a7c | |||
| ef09b108da | |||
| 7f74ff25a3 | |||
| b49cda5643 | |||
| 0d77b64f6b | |||
| 021710f01c | |||
| fd59c11a5c | |||
|
|
82b0522c5b | ||
|
|
ea511811ab | ||
| b9c8b298e9 | |||
| baead81ee9 | |||
| 168b35707b | |||
| 2d699fab86 | |||
| a0d372404c | |||
| 22f0bf1b71 | |||
| 3e4d431bd9 | |||
| d05dcf615e | |||
| 877fb9e19a | |||
| 5e4c0d2dac | |||
| de15937ccf | |||
| e51a310010 | |||
| 6034dfb0c3 | |||
| 8f46bcb93b | |||
| 58f1f1a2c8 | |||
| 66301f4307 | |||
| a9f349a57d | |||
| a1ec2601c8 | |||
| a0f9c7811f | |||
| b8fce891de | |||
| 7effe5fb1a | |||
| b080612396 | |||
| 96c048860d | |||
| 7e75efffd2 | |||
| 910b87fd3a | |||
| a3ec30ed96 | |||
| 8ef46e086b | |||
| a949b2e495 | |||
| 6a662a165c | |||
| 29897b40bc | |||
| d70db5ea7e | |||
| 7206350c34 | |||
| 29ac8c311c | |||
| 74959276b6 | |||
| d61d585f91 | |||
| 3fa951017b | |||
| 9c7cd79e58 | |||
| 658ae8a368 | |||
| fb07e679e8 | |||
| f0a284e36b | |||
| c3cfd512d7 | |||
| 99af60bc16 | |||
| 854805fbbd | |||
| b945c831a0 | |||
| 2b52eaa330 | |||
| f3fc9a3a92 | |||
| 4398ebe654 | |||
| 1a60f82de9 | |||
| bb9208cbcc | |||
| 119771eadb | |||
| 973c9e3a78 | |||
| 675c4806ea | |||
| 255677c219 | |||
| 8d5627567d | |||
| 8bbc573149 | |||
| 6a557cbe2c | |||
| a52af3b50a | |||
| 47076b24d3 | |||
| cb6eee4b0b | |||
|
|
5d505cae16 | ||
| 1c6a978748 | |||
| 9696cdad13 | |||
| fd9f535e63 | |||
| d91f6a8b67 | |||
| c1c4810f3e | |||
| d05deee7ff | |||
| b30a69316a | |||
| 851af7d172 | |||
| 55814ac3a0 | |||
| e6260215a4 | |||
| 6ae107db91 | |||
| 60f876ad08 | |||
| bdafced25a | |||
| 5b283b6bff | |||
| 740cbd291f | |||
| d203b9aa8f | |||
| 52d358c9d8 | |||
| 20fca011a1 | |||
| fb90349670 | |||
| 90a7df6f4b | |||
| f997d1c9a9 | |||
| 24fa9da107 | |||
| ff439d9395 | |||
| 6100a3e8ed | |||
| 3473320ad0 | |||
| 3e8b3e80fd | |||
| a76da4f783 | |||
| ad4fc51d21 | |||
| bcea1ea4ac | |||
| f6d2e44565 | |||
| 634a08c3ab | |||
| 690da98510 | |||
| f94841e34f | |||
| fc8248b854 | |||
| b3c1db9380 | |||
| 72e8cbd2db | |||
| 827165e6fd | |||
| 0d906b1471 | |||
| d1607328fd | |||
| aae47c6e2e | |||
| 4329ab2d7b | |||
| c46e24f820 | |||
| 3e9f6e8461 | |||
| 4315584f79 | |||
| 1a8ed97cfe | |||
| 5cc97b1aea | |||
| 1f5c6ed3e3 | |||
| 2a9350b9df | |||
| 6f65a1936a | |||
| 5c12063ca7 | |||
| 11a0bbef66 | |||
| c8fe9bb148 | |||
| e87d46f6a7 | |||
| efac8cec16 | |||
| 9561cb4bdb | |||
| 344bb0738d | |||
| b54b5383cd | |||
| 23788e304b | |||
| 3b28f84591 | |||
| e5f0d22dc6 | |||
| c6d6eb5c75 | |||
| 2e6cec9b38 | |||
| b607d7f7d6 | |||
| e5a2c5d287 | |||
| 11438e394c | |||
| 3f24034c37 | |||
| 7814bee861 | |||
| 16a4e6e63d | |||
| e275f730ec | |||
| 49edf6670a | |||
| ae930c0347 | |||
| c3940a2958 | |||
| f1573c37d2 | |||
| 85b508d4f2 | |||
| f06cb2dc28 | |||
| a85668f39d | |||
| 64258b98d8 | |||
| 8ea14e660a | |||
| 4334f93edc | |||
| 02b1c7a461 | |||
| efb390b713 | |||
| f2bdef341c | |||
| bd6b7c3ebf | |||
| d747b705ce | |||
| dacafcfdee | |||
| e47cd115fd | |||
| 18e625fd88 | |||
| 8e1f8f2155 | |||
| ccf6d45a08 | |||
| 218a65068a | |||
| 0bd3b9e035 | |||
| 6ec92bd29d | |||
| c01f10c474 | |||
| 0b2094f43f | |||
| 165dc21842 | |||
| e53f6a3f46 | |||
| bda86be02a | |||
| 220f996928 | |||
| 6a34dee598 | |||
| b6db15ca03 | |||
| 75241d0774 | |||
| 7f1b1727d4 | |||
| 25e41c1fba | |||
| c709c3e9df | |||
| 8fd451f6ba | |||
| 44c751154c | |||
| e33a19d38e | |||
| 220bdd6c1f | |||
| d476b2fb18 | |||
| 49a6fb5af8 | |||
| c30680fcf3 | |||
| 361ceb8393 | |||
| 4f8eb711f3 | |||
| 4cf7c85191 | |||
| 6286b83bda | |||
| 6907b95d11 | |||
| d6eaf52ce1 | |||
| 0a05bed028 | |||
| fdfc404818 | |||
| 0fc5871169 | |||
| 36cfb72e41 | |||
| 6e62c0166d | |||
| ff6fd7632f | |||
| 0566672615 | |||
| 9f5bec6a87 | |||
| 560b8d8213 | |||
| 7da61e8fd5 | |||
| f839c9260e | |||
| 39d7002b7a | |||
| b985db14ff | |||
| 1001d24180 | |||
| 039824dcfa | |||
| 4674b6023a | |||
| 1f21c59c19 | |||
| b41032107a | |||
| 6739e19fea | |||
| b2891ff4d2 | |||
| dba06bc47a | |||
| b24d5bbbe0 | |||
| bb499b7fd5 | |||
| 845683c9c9 | |||
| 04064a9fb7 | |||
| a273cb570a | |||
| 73414e7550 | |||
| 6c55a21df5 | |||
| 6e4ef4d557 | |||
| 87a31fba45 | |||
| c8d0e7d352 | |||
|
|
3fcdb8bd9a | ||
| cc478872ae | |||
| 20dfad720c | |||
| c8fd83a80d | |||
| dac89d9e1c | |||
| a0cb78cf28 | |||
|
|
8d3e977f04 | ||
| 26b7d7a080 | |||
| e2e51daca5 | |||
|
|
cdbe51dcb2 | ||
| 9c02d0c996 | |||
| 9b1036872e | |||
|
|
3260b3e520 | ||
| a74dac2066 | |||
| 6cc3546dc2 | |||
|
|
189ad32c4c | ||
| 1f82263d74 | |||
|
|
6aef15bdf7 | ||
| ddfe6a7a74 | |||
|
|
2029611010 | ||
| a2e342cfd2 | |||
|
|
b5764cb52c | ||
| e9b20cdd44 | |||
|
|
28b1c3e91e | ||
| f4ac537c5a | |||
|
|
9d1c227faf | ||
| 874e26201f | |||
|
|
268cee46fb | ||
| a66217282f | |||
|
|
f5da2f6aa8 | ||
| d4c268e6e4 | |||
|
|
72514ca678 | ||
| a54ad6ab04 | |||
|
|
434d3aba6a | ||
| 9332e1b44b | |||
|
|
1282c41e41 | ||
| 8094a75077 | |||
|
|
323363e701 | ||
| c91931380f | |||
|
|
dde96f212e | ||
|
|
8fae0dab54 | ||
| 997fa0501d | |||
| 1567f94184 | |||
| 7b379a03d6 | |||
| 01b35e7b95 | |||
| 76acf259c2 | |||
| 9eb2d68b92 | |||
| cd3bbcadf3 | |||
| 00184bdbec | |||
| c7cc7bbe33 | |||
| b936b2dbf7 | |||
| 5bd107cb46 | |||
|
|
69d4f0bd5c | ||
| 47eae4a774 | |||
| db5d5dbcf1 | |||
| 41dbdadaa2 | |||
| e1b2e6ccbb | |||
| 79c6166181 | |||
| 55955867af | |||
| 55c7d925a6 | |||
| 982a27ce38 | |||
| aef5b00b85 | |||
| 03346907e4 | |||
| a870eb380e | |||
| 4c6777dc68 | |||
|
|
1a315317e7 | ||
|
|
04598482fb | ||
| be0497e6de | |||
| 7db7a73d65 | |||
| 3ade5619d0 | |||
| a48f43607b | |||
|
|
27ba48c1a1 | ||
| c04a9d0c1c | |||
| 3009ef93d9 | |||
|
|
78ad554ece | ||
| 895356dc00 | |||
| 4063ae1a95 | |||
|
|
77da033371 | ||
| 3850da5479 | |||
| 040e460876 | |||
| 55f9e4c803 | |||
| 257fc95182 | |||
|
|
591106ec65 | ||
|
|
15a668829b | ||
| 73b8ad36b1 | |||
| d0db3e87fd | |||
| 05c659b4ca | |||
| ded89934d0 | |||
|
|
9a843cec72 | ||
| 8988a1078a | |||
| bfc28f1081 | |||
| 05eedca6e8 | |||
| 40aa2a6a63 | |||
|
|
5e2321e2f9 | ||
| 464777884b | |||
| cbaa35bcb7 | |||
| 143e84f758 | |||
|
|
1e76e70662 | ||
| 4ea3f7b194 | |||
| bf293eff2f | |||
| 757bc292f9 | |||
| 381eb3b8f5 | |||
| f9affe1e8b | |||
| eea4632642 | |||
|
|
c7d354a153 | ||
| 8e009e0aa6 | |||
|
|
e0cd433485 | ||
| 8ffe54ee0d | |||
| 449b28f8a1 | |||
| 4ef6d3aa76 | |||
| af76e33b45 | |||
|
|
8df8f4d181 | ||
| 70d35eb7f2 | |||
| 73795bb3c3 | |||
| 03fdc8fe14 | |||
|
|
223f81ac63 | ||
| bf022f618f | |||
| 9df984c73f | |||
| 5acfdb17c6 | |||
| 364b8c4a30 | |||
| ba933a6ec3 | |||
| 67e85ba4dd | |||
| 5fb67f5b88 | |||
|
|
c870b6362b | ||
| de15a18c30 | |||
| f65af3355c | |||
|
|
7355e606ca | ||
| e594f681a4 | |||
| 0db96a8beb | |||
| 667d8a09e0 | |||
| 170619053f | |||
|
|
25f6c19586 | ||
| 6dcf978e66 | |||
| df052796d9 | |||
| 5fc8785d68 | |||
| 25a3afaff1 | |||
|
|
84f0a37fdf | ||
| ca9b38b175 | |||
| 8a4996d14c | |||
|
|
da71515d79 | ||
| 73e79b85b4 | |||
|
|
56d1bee9fb | ||
| 6daacb1987 | |||
|
|
69f19e72da | ||
| be7704bc1a | |||
|
|
051ca6d1d7 | ||
| 69659f6a67 | |||
| 1bd5108472 | |||
| 6c96ba62e4 | |||
| 4d0cbf8b7f | |||
| 4f96da87d7 | |||
|
|
2b036db0d6 | ||
| 376b4ecd2a | |||
| a3cd643da4 | |||
|
|
f9e2032750 | ||
| e39123dc7e | |||
| d5408536b3 | |||
| 12277f3c08 | |||
| 976db154a0 | |||
| d6dda15024 | |||
| fa1ad79239 | |||
| e23fa9545e | |||
| 96836d865c | |||
| 3243e0d2f2 | |||
| 3dfe85f547 | |||
| 221abd94a2 | |||
| ebb6b3067c | |||
|
|
6160ec9aaa | ||
| f30b3b39f8 | |||
|
|
f69384f1e0 | ||
| 56baace9d1 | |||
| 53df892193 | |||
| 1139ce103e | |||
|
|
173b67c495 | ||
| e8cebe1662 | |||
| 3b0423189e | |||
| 742897700c | |||
| 296cebc69e | |||
| 4160167e5c | |||
| d836179a72 | |||
| 9d3630574f | |||
| 8664c84893 | |||
| 96e04bbe0f | |||
| 09e16f35a1 | |||
| 7834a29724 | |||
| ab8822e3f4 | |||
|
|
9950e9ec3e | ||
| 951af764fb | |||
| 6b7d7f8607 | |||
| 68490a4a9b | |||
|
|
a44d9c51d8 | ||
| 013385a6c4 | |||
| 6ee7fe3cbc | |||
|
|
2f9b16f05a | ||
| ce571233d7 | |||
| 89b962aa8c | |||
| d8edf050f5 | |||
| 7349288e88 | |||
| 3ec9a82984 | |||
| 1a87993394 | |||
| 7dd39d52a4 | |||
|
|
9d10dd5aea | ||
| df864f2a99 | |||
| a6d4cc1632 | |||
| ea19e80685 | |||
|
|
2b05a31ab2 | ||
| 626acede56 | |||
| 2f3785d7fe | |||
| 8c932b1b2b | |||
| bf0df9b0a1 | |||
| c536d71657 | |||
| 8f8dfb137c | |||
| 765de07ac8 | |||
| c687225e1b | |||
|
|
2f6ca5478b | ||
| 1d4b137dad | |||
| 90512da938 | |||
| 981acc4b44 | |||
| bd54d82ae2 | |||
|
|
abba571443 | ||
| 30eeedf86e | |||
|
|
50c4ea59d0 | ||
| 15898cbb9d | |||
| bf64995eda | |||
| a2e7f1103d | |||
| 9f47542e2f | |||
| 65da37a486 | |||
| 9094b202c9 | |||
| 555176fef5 | |||
| 572caab035 | |||
| 00740feb43 | |||
| e822167a49 | |||
| 1c26ad6bbd | |||
| 6694860779 | |||
| 876becec0a | |||
| 644996b9f5 | |||
| 56241026e1 | |||
| c4f60fd166 | |||
| f3afa4dc2b | |||
| 6b111adb18 | |||
| 3370d9bfbb | |||
| 751195d8fc | |||
| 089c4791fe | |||
| 5113fe1e60 | |||
| abc5dd47cb | |||
| aac1ae58d9 | |||
| 0c0c592b0d | |||
| 9fa3c9ff0d | |||
| 095d5b9da1 | |||
| 82aa9c3def | |||
| 3915f6ea41 | |||
| 23ce1c16ba | |||
| fea1147bf6 | |||
| 31dc83f696 | |||
| cfdb1c418c | |||
| 41fb5e6102 | |||
| d8c34b2811 | |||
| 10565ad4e9 | |||
| cb21023e4d | |||
| 833905a1dd | |||
| 64c547aeea | |||
| f3dc9e9bb2 | |||
| 80dc9e6c06 | |||
| 78a049c4b2 | |||
| ee3a9aee4d | |||
| 8323db2690 | |||
| b3e842838f | |||
| 2235c63986 | |||
| 61224031e2 | |||
| c0e3ff5858 | |||
| c078f66d8f | |||
| 01715363aa | |||
| 76cc96eff5 | |||
| 4caf79be55 | |||
| a560f626d0 | |||
| 11d589c19b | |||
| b71a815cc4 | |||
| c89c632caa | |||
| f5fcc2f9aa | |||
| 110f060446 | |||
| 47773a63ab | |||
| 9315a01a4f | |||
| 931c936d9b | |||
| 0b1d139e40 | |||
| 83d3f136a3 | |||
| e2dcce4e9f | |||
| 2e2ab3995f | |||
|
|
c9397e3008 | ||
|
|
206157047e | ||
|
|
bed4c1c6d3 | ||
|
|
9ba49e3bf7 | ||
| ebf714f123 | |||
| 5763aa3a73 | |||
| 2a8c1b33de | |||
| 9d1b0abade | |||
| 41d78ef455 | |||
| fbd474b2c3 | |||
| c47e454532 | |||
| e12790efbd | |||
| ce636273a6 | |||
| 4dd87eae2f | |||
| af02a9731c | |||
| 663715af0f | |||
|
|
05d6135e53 | ||
|
|
0c57960fd3 | ||
|
|
7c3edd134c | ||
| aa3a9f5443 | |||
| a0748c2d43 | |||
| bb24a7d167 | |||
| 4007d7f8ca | |||
| 670f115a62 | |||
| 6899b18ff8 | |||
| 6a74f3c7d0 | |||
| e75fcd5ea6 | |||
| 80112f5125 | |||
| 9adaa75190 | |||
| 3e487adf2c | |||
| 5aef304ed7 | |||
| 0b71a0e8b4 | |||
| 03df656d36 | |||
| 4117458a70 | |||
| 5ba90bebf3 | |||
| 62b0545343 | |||
| d282c571bf | |||
| 1e8a41c33b | |||
| afde96111a | |||
|
|
f5c7f6f6b5 | ||
| d889d32714 | |||
|
|
4a8ee46c2d | ||
| 2c615889cf | |||
| da0c5008df | |||
|
|
994b48f39d | ||
| e13cf925f1 | |||
| 6dba5a08d6 | |||
|
|
211cff09b5 | ||
| 3290f3b7c0 | |||
|
|
72b7eca12e | ||
| 0d1c49307e | |||
|
|
8bb546c689 | ||
| 05d6f97b44 | |||
| 28a73c8477 | |||
| c64a666fa5 | |||
| 5325cddade | |||
|
|
1d7ca0cd22 | ||
| a610ffd276 | |||
| 5800ad713d | |||
| 4fd0d864ee | |||
|
|
f99f071642 | ||
| 77b6f2624d | |||
| d9fea2f9b4 | |||
| 65a1a8e494 | |||
|
|
8fc5dba929 | ||
| 919a88c6be | |||
| 60eeb79185 | |||
|
|
91cba0bbda | ||
| eb7fdb01e0 | |||
|
|
2c6e6f2b51 | ||
| ca1a2ede1d | |||
| e1f22b6dda | |||
| 7a56b8de35 | |||
| 4922aada7f | |||
|
|
2523333fc8 | ||
| 8b22d0ff62 | |||
|
|
f5a4c73248 | ||
| d8a75487ac | |||
|
|
630cb81e03 | ||
| 0f3f57e7bb | |||
|
|
7c14bdfe5e | ||
| 062131608f | |||
|
|
1de8b29865 | ||
| 3b8daf964e | |||
|
|
c043c5fca8 | ||
| 10d22f05b7 | |||
|
|
5ece3f1208 | ||
| 5ca4ecf455 | |||
|
|
ace77976fa | ||
| 4ffd66bb2d | |||
|
|
9b8144073f | ||
| 2f04b0fa84 | |||
|
|
270f655d3a | ||
| b366946855 | |||
|
|
433c5ca190 | ||
| 1a23c1f78a | |||
|
|
ff0503ea70 | ||
| 675f6ae458 | |||
|
|
b689c235f6 | ||
| d26992d905 | |||
|
|
295f77601d | ||
| f176fa3173 | |||
|
|
18f727b7d7 | ||
| 9bd5a958f2 | |||
|
|
bbcfe7ad1a | ||
| dbe255c31e | |||
|
|
52e51b2766 | ||
| 87614c7143 | |||
|
|
93cf0ebafa | ||
|
|
94f40c9162 | ||
| c1fa245a6a | |||
| 6e83a4a065 | |||
|
|
92b203cbbc | ||
| 7bd265fe1a | |||
|
|
dc6505a2f2 | ||
|
|
51ed62f10a | ||
|
|
dfa7e34ad0 | ||
| 2a51348b9d | |||
|
|
4cb8fb87fa | ||
| 8d48d86bcd | |||
|
|
7a651f360d | ||
| 81da97e53e | |||
| 2647cfffa1 | |||
| e4d59569eb | |||
|
|
d2cb735f09 | ||
| 9b7c6f67f1 | |||
| 4289f8ff3c | |||
|
|
008da01444 | ||
| 68e2723747 | |||
|
|
b8da257b83 | ||
| b2ee2a97ef | |||
| c77e6250a9 | |||
|
|
e7a84f0380 | ||
| e67baf4cd7 | |||
|
|
110133bee9 | ||
| ac14e65353 | |||
| 1351054927 | |||
|
|
46dfd781d3 | ||
| cf3aef48c8 | |||
|
|
198fecea11 | ||
| ab67c981dd | |||
|
|
92dee2d2d0 | ||
|
|
a3666c4ba4 | ||
| e128505dfb | |||
| 725de94548 | |||
| 154872da9f | |||
| 3c289c50b9 | |||
| 8f2a5c1608 | |||
| 9a275e2355 | |||
| 2dab321e36 | |||
|
|
982176209a | ||
| de8e6de23c | |||
|
|
ffa75bbe9d | ||
| 9697ea05a9 | |||
|
|
f34dc633a8 | ||
| 1063c91815 | |||
| a5c07c3d50 | |||
|
|
febf58fd27 | ||
| 0d1d3475db | |||
| 66b608d760 | |||
|
|
3a0d60c588 | ||
| 8bf5fe4f9a | |||
| 9f850bef53 | |||
| a93d949ede | |||
| f945c393b8 | |||
| d8bc4acd90 | |||
| 39ec84eaed | |||
| 78ed66c7cd | |||
| 0aeedcf1f4 | |||
| 8a4598e2b3 | |||
| 720d61a9d3 | |||
| d195408fd4 | |||
| 8dc1449eab | |||
| fc22b64b99 | |||
| 693ee360e9 | |||
| bafd856d1e | |||
| 6b230d464d | |||
| 2b1d2173c0 | |||
|
|
4339590eb1 | ||
| 13faf609f7 | |||
| 8263bc7681 | |||
|
|
a6f44807c6 | ||
|
|
3ebb4477cc | ||
| b7666e5f57 | |||
| 3d52b63d26 |
1005
.claude/HISTORY.md
|
|
@ -383,9 +383,13 @@ case "$1" in
|
||||||
adduser --system --group --no-create-home --home /var/lib/secubox secubox
|
adduser --system --group --no-create-home --home /var/lib/secubox secubox
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Répertoires runtime
|
# Répertoires runtime — SHARED parents, NE JAMAIS les passer en 0750/0700
|
||||||
install -d -o secubox -g secubox -m 750 /run/secubox
|
# (#623 : casse la traversée pour les daemons non-secubox → kbin/toolbox 500).
|
||||||
install -d -o secubox -g secubox -m 750 /var/lib/secubox
|
# /run/secubox reste 1777 (sticky world-writable, sockets de tous les services,
|
||||||
|
# #471) ; /var/lib/secubox reste 0755. Les leaves privées
|
||||||
|
# (/var/lib/secubox/<module>) peuvent être 0750.
|
||||||
|
install -d -o root -g root -m 1777 /run/secubox
|
||||||
|
install -d -o secubox -g secubox -m 755 /var/lib/secubox
|
||||||
|
|
||||||
# Activer et démarrer le service
|
# Activer et démarrer le service
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
|
|
|
||||||
252
.claude/TODO.md
|
|
@ -1,10 +1,260 @@
|
||||||
# TODO — SecuBox-DEB Backlog
|
# TODO — SecuBox-DEB Backlog
|
||||||
*Mis à jour : 2026-06-09*
|
*Mis à jour : 2026-06-27*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚪ T5 — Images / OS variants / Hardware (ajouts 2026-06-27)
|
||||||
|
|
||||||
|
### ⬜ MOCHAbin — bootloader propre (adresses réservées + extlinux)
|
||||||
|
|
||||||
|
> Workaround actif : `/boot/boot.scr` compilé forçant le kernel à `0x0a000000`. Fix durable requis.
|
||||||
|
|
||||||
|
- [ ] **Option A — Corriger l'image** : patcher `extlinux.conf` généré par le CI pour utiliser
|
||||||
|
`0x0a000000` (kernel) et `0x10000000` (initrd) au lieu de `0x02080000` (adresse réservée
|
||||||
|
factory U-Boot 2020.10 → reset immédiat). Boot.scr deviendrait redondant.
|
||||||
|
- [ ] **Option B — Enhanced Tow-Boot (#748)** : bloqué par le ciseau U-Boot (voir ci-dessous) ;
|
||||||
|
déverrouille wget/HTTP natif dans U-Boot, supprime le besoin de TFTP pour les futures installs.
|
||||||
|
- [ ] **Valider** que le fix d'adresse tient sur les deux MOCHAbin (gk2 + c3box).
|
||||||
|
|
||||||
|
### ⬜ #748 — wget dans U-Boot pour MOCHAbin (bloquant documenté)
|
||||||
|
|
||||||
|
> Bloquant dur (ciseau) confirmé 2026-06-27. Branche
|
||||||
|
> `feature/748-enhanced-tow-boot-http-netboot-serial-fl` : spec + plan + Kconfig +
|
||||||
|
> `build-uboot-overlay.sh --tow-boot` + CI `.github/workflows/build-tow-boot.yml` en place.
|
||||||
|
> Problème : board mochabin UNIQUEMENT dans fork Tow-Boot U-Boot 2022.07 (pas de `wget`) ;
|
||||||
|
> `wget`/TCP UNIQUEMENT dans stock U-Boot ≥2023.07 (pas de board mochabin/DTS).
|
||||||
|
|
||||||
|
- [ ] **Voie 1** : backporter le stack TCP + `wget` de U-Boot ≥2023.07 dans le fork Tow-Boot
|
||||||
|
2022.07 (mochabin board natif). Diff TCP = `net/wget.c` + dépendances `CONFIG_NET_WGET`.
|
||||||
|
- [ ] **Voie 2** : porter le board mochabin (DTS Armada 7040 + PHY + eMMC) vers U-Boot mainline
|
||||||
|
≥2023.07 (sans Tow-Boot). Plus long mais durable.
|
||||||
|
- [ ] Choisir une voie, débloquer #748.
|
||||||
|
|
||||||
|
### ⬜ Packager le flow netboot + install signé (rig temporaire → procédure reproductible)
|
||||||
|
|
||||||
|
> Actuellement rig manuel sur gk2 : `lan1=192.168.77.1/24`, dnsmasq DHCP, nft, nginx `:8099`.
|
||||||
|
|
||||||
|
- [ ] Scripter la publication de l'image signée dans le root HTTP netboot (wget + sha256 + sig).
|
||||||
|
- [ ] Documenter / packager la config dnsmasq + nft + nginx pour un segment `lan1` dédié.
|
||||||
|
- [ ] Intégrer dans `scripts/deploy-netboot.sh` ou équivalent.
|
||||||
|
|
||||||
|
### ⬜ Teardown rig netboot temporaire gk2
|
||||||
|
|
||||||
|
> Le rig (lan1 bridge, dnsmasq, nft iif lan1 accept, nginx extra listen) reste actif jusqu'à
|
||||||
|
> ce que c3box soit autonome en prod.
|
||||||
|
|
||||||
|
- [ ] Retirer la règle nft `iif lan1 accept` (risque : tout le segment lan1 est accepté sans filtrage).
|
||||||
|
- [ ] Désactiver / retirer dnsmasq test sur lan1.
|
||||||
|
- [ ] Retirer le extra listen `192.168.77.1:8099` du vhost nginx netboot (ou couper le vhost si
|
||||||
|
plus nécessaire).
|
||||||
|
- [ ] Vérifier que c3box auto-boot sans rig (boot.scr en place → OK).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Clos 2026-06-22 — DPI exfil + report Netrunner + sbxmitm
|
||||||
|
|
||||||
|
- ✅ **#687 DPI exfil pipeline** — flowcap + Go collector + dashboard + cumulatif 7j,
|
||||||
|
packagé `secubox-dpi 1.1.2` (inclut #692/#693/#695/#705).
|
||||||
|
- ✅ **#707 report kbin = fiche Netrunner** HTML+PDF (#699/#701/#703/#709/#711/#714/#716).
|
||||||
|
- ✅ **#689** sbxmitm cert 365d · **#697** stream >8MiB (Gmail) · **#688** splice rejeté.
|
||||||
|
|
||||||
|
### DPI Phase 3
|
||||||
|
- [x] Enrichissement **ASN** (GeoLite2-ASN) pour l'egress sans SNI — **#719 mergé, live**
|
||||||
|
(`secubox-dpi 1.1.3`, maxminddb-golang vendored).
|
||||||
|
- [x] **Historique + timeline par device** — **#721 mergé, live** (`secubox-dpi 1.1.4`,
|
||||||
|
buckets quotidiens `history.json` 14j + `/api/v1/dpi/history` + panneau Timeline
|
||||||
|
dashboard). NB : JSON daily buckets (pas SQLite — pas de driver CGO dans le binaire
|
||||||
|
statique ; SQL riche reportable si besoin).
|
||||||
|
- [x] Démon **nDPId** — **évalué puis ÉCARTÉ** (#722/#723 revertés). Raison perf :
|
||||||
|
ndpiReader tourne en fenêtres bornées (Nice 15, ~1% CPU, libère le cœur entre
|
||||||
|
les passes) ; nDPId = démon permanent + nDPIsrvd → CPU/RAM **continue** sur une
|
||||||
|
board déjà saturée (load ~4.6/4 cœurs). Gain (JSON riche, pas de respawn) <
|
||||||
|
risque. **Décision : on garde ndpiReader** comme producteur du pipeline exfil.
|
||||||
|
(Le build CI QEMU a aussi échoué au 1er essai → chemin fragile en plus.)
|
||||||
|
|
||||||
|
### ⬜ Cosmétique report PDF (non bloquant)
|
||||||
|
- [ ] Glyphes drapeaux régionaux → lettres (police embarquée). Option : drapeaux PNG.
|
||||||
|
- [ ] Chiffres espacés dans certaines cellules (fallback police).
|
||||||
|
|
||||||
|
### ⬜ APK on-device #685/#686 — NON-ROOT ONLY (plan verrouillé, à faire)
|
||||||
|
> Décision 2026-06-22 : cible **non-root uniquement** ; chemin root abandonné.
|
||||||
|
> Plan détaillé : commentaire #685.
|
||||||
|
- [ ] **VpnService in-app** (`com.wireguard.android:tunnel` / GoBackend wireguard-go)
|
||||||
|
— l'APK EST le client WG, plus de Play Store, détection tunnel in-app fiable.
|
||||||
|
- [ ] **CA en DER** (fix « nom de cert vide » du KeyChain intent) + `network-security-config`
|
||||||
|
pour que la WebView in-app fasse confiance au CA ca-wg.
|
||||||
|
- [ ] Retirer RootShell/RootOnboard/BootReceiver ; manifest VpnService + consent VPN.
|
||||||
|
- [ ] Limite Android : pas de CA **système** sans root → MITM système impossible ;
|
||||||
|
surface « safe browsing » = WebView in-app. À documenter.
|
||||||
|
- [ ] Build via CI `build-android-apk` + **test sur appareil** (gros build, itératif).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Backlog priorisé — revue 2026-06-24 (64 issues ouvertes)
|
||||||
|
|
||||||
|
> Index d'autorité du triage. Les sections « Phase X » plus bas sont historiques :
|
||||||
|
> plusieurs portent « ✅ COMPLETE » alors que l'issue est restée **ouverte** (livré
|
||||||
|
> mais jamais fermé) → marquées **[vérifier→fermer]** ci-dessous.
|
||||||
|
|
||||||
|
### 🔴 T0 — Régressions & bugs sécurité (petits, débloquants, CSPN priv-sep)
|
||||||
|
- #494 secubox-core ExecStart écrase tmpfiles.d `/run/secubox` *(worktree actif)*
|
||||||
|
- #468 `/etc/secubox` parent 0750 casse la traversée non-secubox *(régression récurrente)*
|
||||||
|
- #471 secubox-mesh postinst écrase perms `/run/secubox` *(régression)*
|
||||||
|
- #421 sockets `/run/secubox` cachés en mount-ns privé (RuntimeDirectory)
|
||||||
|
- #447 kiosk : mot de passe admin semé par le CI (users.json shippe un hash) **← fuite**
|
||||||
|
- #91 haproxyctl régénère haproxy.cfg avec `waf_inspector` inexistant *(intégrité WAF)*
|
||||||
|
- #65 nginx : routes API manquantes dans webui.conf
|
||||||
|
- #53 Wazuh uvicorn 100% CPU spin
|
||||||
|
- #121 metablog ingest : dirs en `secubox:secubox`
|
||||||
|
|
||||||
|
### 🟠 T1 — Plan d'enforcement sécurité (mission CSPN ; détection→action)
|
||||||
|
- #498 Phase 7 — WAF active enforcement (mitm→CrowdSec→nft drop) *(worktree actif)*
|
||||||
|
- ✅ #519 Phase 13 — enforcement plane **FERMÉ 2026-06-22** (livré + réparé :
|
||||||
|
blacklist-sync avortait sur NXDOMAIN + timeout unit → fix `|| true` +
|
||||||
|
TimeoutStartSec 600 ; vérifié live, default-off). Inclut 13.B #522.
|
||||||
|
- #455 secubox-egress — détection egress + corrélation RDS multi-signaux
|
||||||
|
- #500 Phase 8 — Utiq operator-grade tracking (detect/alert/bypass)
|
||||||
|
- #514 Phase 12 — plateforme anti-human-detection (parent ; sous-tracks fermés)
|
||||||
|
- ✅ #515 Phase 12.A CDN cache detection — **FERMÉ** (live, `social_host_meta.cdn_vendor`)
|
||||||
|
- ✅ #516 Phase 12.B anti-bot detection — **FERMÉ** (live via #564/#565, `social_antibot`)
|
||||||
|
- #525 Phase 14 — plan de déception (idée future, parké)
|
||||||
|
- ⬜ Suivi #519 perf (non bloquant) : DNS-guard ne résout que les 2000 premiers
|
||||||
|
domaines/cycle (5523 en base) → couverture partielle ; résolution séquentielle
|
||||||
|
lourde sur board saturé. Option : résolution parallèle bornée + rotation du cap.
|
||||||
|
|
||||||
|
### 🟡 T2 — UX / Hub / conscommateurs report (worktrees actifs + polish)
|
||||||
|
- #615 security-posture dans la sidebar Hub *(worktree actif)*
|
||||||
|
- #655 webext content-script banner CSP-immune *(worktree actif)*
|
||||||
|
- #485 toolbox SOC scoring *(worktree actif)*
|
||||||
|
- #513 ToolBox WebUI : sous-onglets + retrait UI /admin redondante
|
||||||
|
- #69 diagramme flux trafic responsive
|
||||||
|
- #67 cache history-aware glances/netdata
|
||||||
|
- #68 health checks + dépendances services au démarrage
|
||||||
|
|
||||||
|
### 🟢 T3 — Backlog feature (valeur, non bloquant)
|
||||||
|
- #685 APK 'corrupt' — CI signe avec clé éphémère *(plan APK verrouillé)*
|
||||||
|
- #686 android-toolbox flux non-root cassé *(plan APK verrouillé)*
|
||||||
|
- #429 nextcloud dashboard : API stubs au lieu de la vraie instance *(bug)*
|
||||||
|
- #430 nextcloud — fédération OCM (doc/outillage)
|
||||||
|
- #472 nextcloud — Gondwana Desktop (canvas + widgets)
|
||||||
|
- #592 secubox-webmail-hub (Gmail OAuth2 + Gandi + OVH)
|
||||||
|
- #66 auth Google OAuth
|
||||||
|
- #70 Health Banner System *(preplanned)*
|
||||||
|
- #71 CDN proxy injection *(preplanned)*
|
||||||
|
- #393 source-home des scripts health prober
|
||||||
|
|
||||||
|
### 🔵 T4 — Hardware-gated (dépend de pièces ; piste parallèle ; pas de spare EP06)
|
||||||
|
- Modem/PCIe : #254 modules kernel LTE · #255 pins mPCIe modem · #460 DTS cp0_pcie2 ·
|
||||||
|
#467 U-Boot comphy5 SerDes · #462 pivot HW AR9271/MT
|
||||||
|
- Mesh/BLE : #449 WiFi 802.11s · #452 BT mesh · #453 QR multi-canaux · #454 sourcing BLE 5.x
|
||||||
|
- GSM : #347 sentinelle-gsm
|
||||||
|
- Smart-Strip : #33 module HMI · #42 sous-repo · #379 packaging
|
||||||
|
- Eye-remote : #41 sous-repo · #79 buildroot · #127 variante square · #138 radar_concentric ·
|
||||||
|
#155 collision link-rename *(bug)* · #158 multi-gadget L3 · #478 métriques live Round Eye
|
||||||
|
- VILLAGE3B : #480 dossier presse · #497 poster grand public
|
||||||
|
|
||||||
|
### ⚪ T5 — Images / OS variants (basse urgence)
|
||||||
|
- #446 Full Traveller OS multi-mode/arch · #125 build-live-usb +virtualbox · #422 vm-x64 cascade
|
||||||
|
|
||||||
|
### ⚫ T6 — Docs / housekeeping
|
||||||
|
- #81 headers SPDX CMSD-1.0 partout · #243 clarifier scope secubox-zkp-auth *(question)*
|
||||||
|
- #474 ToolBoX (epic parent — garder comme tracker)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔥 P0 — Immediate (in flight)
|
## 🔥 P0 — Immediate (in flight)
|
||||||
|
|
||||||
|
### kbin Tor endpoint — anonymized quick-switch surfing (#683)
|
||||||
|
|
||||||
|
> Capstone du couteau suisse cyber : l'anonymat de la sortie. Spec :
|
||||||
|
> `docs/superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md`.
|
||||||
|
> Invariants : inspection préservée, fail-closed, opt-in (défaut OFF), no DNS leak, CSPN audit.
|
||||||
|
|
||||||
|
- [ ] **Transport** — Option A dialer SOCKS5 upstream (cœur Go #662, *préféré*) vs
|
||||||
|
Option B nft mark → Tor TransPort (fallback pré-#662).
|
||||||
|
- [ ] **Profil Tor egress** — réutiliser `secubox-exposure` (bootstrap/NEWNYM), egress-only.
|
||||||
|
- [ ] **API toolbox** — `POST /admin/tor/{on,off}` (WG-hash scoped) + `GET /tor/state` +
|
||||||
|
`POST /tor/newnym` + état SQLite per-client (TTL 24h).
|
||||||
|
- [ ] **UI kbin** — toggle 🧅 + badge état + flag pays de sortie + bouton « nouvelle identité ».
|
||||||
|
- [ ] **Leak-guard nft** + DNS-over-Tor (test exit IP + resolver ≠ Unbound).
|
||||||
|
- [ ] **`tls_splice` OFF en mode Tor** (#649) — sinon les flux asset fuient l'IP réelle.
|
||||||
|
- [ ] **CSPN** — audit-log chaque bascule ; soak DARK (flag présent, UI cachée) avant flip.
|
||||||
|
|
||||||
|
### ToolBox clients (`clients/`)
|
||||||
|
|
||||||
|
- [x] **#531 Android scaffold + CI** — Gradle/Compose one-tap onboarding,
|
||||||
|
debug APK via `build-android-apk.yml`. CI green.
|
||||||
|
- [x] **#536 serve APK from toolbox** — `GET /wg/toolbox.apk` + onboard button +
|
||||||
|
`secubox-toolbox-fetch-apk` helper.
|
||||||
|
- [x] **#538 Android root-mode silent** (PR #539) — system CA install + native
|
||||||
|
kernel WireGuard + auto R3 verify, gated behind explicit root tap.
|
||||||
|
- [x] **#532 browser extension** (`clients/webext-toolbox/`) — MV3 Firefox
|
||||||
|
`.xpi`/Chromium; live tracker badge + popup mini Round-Eye graph over
|
||||||
|
`/social/*`; `GET /wg/toolbox.xpi` + fetch helper + `build-webext.yml`.
|
||||||
|
- [x] **#532 release** — tag `webext-v0.1.1` published the `.xpi`
|
||||||
|
(downloadable, verified 200). `make_latest:false` + tag-pinned URL so it
|
||||||
|
doesn't steal "Latest" from the Android APK release.
|
||||||
|
- [ ] **release signing** — Android keystore + AMO `.xpi` signing secrets in CI
|
||||||
|
for stable published fingerprints (currently unsigned sideload).
|
||||||
|
- [ ] **#532 follow-ups** — optional `GET /social/live/{token}` SSE (replace the
|
||||||
|
client-side poll) ; Poke/Emancipate per-site control once #525 (deception)
|
||||||
|
ships ; Chromium PNG icon rasterisation for the Web Store.
|
||||||
|
|
||||||
|
### Phase 13 — Protection enforcement plane (#519) — ✅ COMPLETE
|
||||||
|
|
||||||
|
- [x] **13.A spine** (#521, `2.6.8`, v2.13.17) — nft blacklist set + forward-drop
|
||||||
|
chain + sync (CrowdSec + threat-intel). + override_dh_strip drift fix.
|
||||||
|
- [x] **13.B DNS-guard** (#522, `2.6.9`, v2.13.17) — résout domaines blocklistés
|
||||||
|
→ IPs ; détection DoH/DoT (block opt-in).
|
||||||
|
- [x] **13.C attribution** (#524, `2.6.10`, v2.13.18) — per-device blocked-attempts
|
||||||
|
+ quarantine + endpoints + tile.
|
||||||
|
- [x] **13.D feedback** (#527, `2.6.11`, v2.13.19) — escalation evaluator
|
||||||
|
(detections→nft/cscli/quarantine), audit-log, **default OFF**.
|
||||||
|
- [ ] **13.x opt-in tuning** — activer `SECUBOX_ESCALATE_*` / `SECUBOX_DOH_BLOCK`
|
||||||
|
selon politique opérateur quand voulu.
|
||||||
|
- [ ] **threatfox feed = 0** — investiguer l'ingestion domain vide (impacte
|
||||||
|
13.B resolved_domains).
|
||||||
|
|
||||||
|
### Phase 14 — Plan de déception (#525, idée future)
|
||||||
|
|
||||||
|
- [ ] Pseudo-réponses proxy au lieu de blocage IP (indistinguable, pollue
|
||||||
|
le profil) + neutralisation des scripts CDN préchargés. R3 consenti,
|
||||||
|
réutilise la détection Phase 11/12. **Pour plus tard.**
|
||||||
|
|
||||||
|
### Phase 11 — Social mapping per device (#502) — ✅ COMPLETE (v2.13.15)
|
||||||
|
|
||||||
|
- [x] **11.A backend** (#505, `2.6.0`) — correlation engine + SQLite + API.
|
||||||
|
- [x] **11.B frontend** (#507, `2.6.1`) — d3 graph + i18n + favicon proxy + wipe.
|
||||||
|
- [x] **11.C evidence + PDF** (#508, `2.6.3`) — consent-probe + bilingue FR/EN PDF.
|
||||||
|
- [x] **Toolbox WebUI tabs** (#513, `2.6.2`) — 5-tab nav, kbin /admin/ supprimé.
|
||||||
|
- [x] **Mergé** via PR #517 → master, tag `v2.13.15`.
|
||||||
|
- [ ] **11.D opérateur** (futur, optionnel) — vue HTML `/admin/social/`
|
||||||
|
dédiée (le tab Cartographie sociale dans /toolbox/ couvre déjà l'agrégat).
|
||||||
|
|
||||||
|
### Phase 12 — Anti-human-detection platform (#514)
|
||||||
|
|
||||||
|
- [x] **12.A CDN** (#515, `2.6.4`) — detect_cdn + Round-Eye central-hotspot
|
||||||
|
graph + by_cdn. Mergé.
|
||||||
|
- [x] **12.B anti-bot** (#516, `2.6.5/2.6.6`) — detect_antibot (détection
|
||||||
|
seule) + ring levels visibles + Carto/Reset opérateur. Mergé.
|
||||||
|
- [x] **12.C opérateur-grade / state-adjacent** (#518, `2.6.7`, v2.13.16) —
|
||||||
|
detect_operator_grade (telco MSISDN/x-acr + consortium Utiq/TrustPid +
|
||||||
|
data-broker LiveRamp/BlueKai/Palantir-class). Top-severity lens + PDF.
|
||||||
|
- [ ] **12.B bypass** — résolution de challenge (gated derrière doctrine
|
||||||
|
lawful-use + design review ; R3 opt-in uniquement).
|
||||||
|
- [ ] **12.D noise counter-measures** — cookie-noising / header-strip /
|
||||||
|
decoy-traffic (gated derrière doctrine ; R3 opt-in, interférence active).
|
||||||
|
|
||||||
|
### Système — bugs gk2 (2026-06-10) — ✅ résolus
|
||||||
|
|
||||||
|
- [x] **CrowdSec firewall** — restart bouncer → tables nft recréées.
|
||||||
|
- [x] **WAF /var/log/secubox traversal** — fix source #511/#512 (mergé).
|
||||||
|
- [x] **WAF /stats perf** (#509/#510, `secubox-waf 1.2.2`) — double-buffer cache.
|
||||||
|
- [x] **PeerTube + PhotoPrism** — LXC redémarrés.
|
||||||
|
- [ ] **Round Eye gadget** — USB CDC-Ethernet TX queue wedged (NETDEV
|
||||||
|
WATCHDOG, probe -110). Recovery gk2 épuisée. **Fix physique : power-cycle
|
||||||
|
Pi Zero / re-seat câble OTG.** Reprendre côté gk2 au prochain boot propre.
|
||||||
|
|
||||||
### Phase 10 — Banner injection perf (#501) — ✅ shipped 2026-06-09
|
### Phase 10 — Banner injection perf (#501) — ✅ shipped 2026-06-09
|
||||||
|
|
||||||
- [x] **Banner perf quick wins** (`secubox-toolbox` 2.5.1, commit `ce059d0f`)
|
- [x] **Banner perf quick wins** (`secubox-toolbox` 2.5.1, commit `ce059d0f`)
|
||||||
|
|
|
||||||
519
.claude/WIP.md
|
|
@ -1,5 +1,522 @@
|
||||||
# WIP — Work In Progress
|
# WIP — Work In Progress
|
||||||
*Mis à jour : 2026-06-09*
|
*Mis à jour : 2026-06-27*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 2026-06-27 : c3box → SecuBox Debian — première install réussie · netboot prouvé (#748 #737)
|
||||||
|
|
||||||
|
### ✅ Fait (session 2026-06-27)
|
||||||
|
|
||||||
|
- **Netboot gk2→c3box prouvé** — factory U-Boot 2020.10 → TFTP → rescue shell installeur
|
||||||
|
(kernel 6.12.85 #5secubox). Détour cabling résolu (impasse LAB, pas logiciel).
|
||||||
|
- **Première install SecuBox Debian sur un MOCHAbin physique (c3box)** — image CI artefact
|
||||||
|
`secubox-mochabin-bookworm` (run 27426515472, 8 Gio), SHA256 + signature vérifiés,
|
||||||
|
`gunzip|dd` en RAM → eMMC. c3box boot Debian v1.9.0 avec stack complète.
|
||||||
|
- **boot.scr workaround déployé** — extlinux.conf charge le kernel à `0x02080000` (réservé
|
||||||
|
factory U-Boot → reset). Construit `/boot/boot.scr` (kernel@`0x0a000000`) ; auto-boot
|
||||||
|
Debian sans intervention vérifié après reboot.
|
||||||
|
- **#748 bloquant documenté** — ciseau U-Boot : mochabin board UNIQUEMENT dans fork Tow-Boot
|
||||||
|
2022.07 (pas de `wget`) ↔ `wget` UNIQUEMENT dans stock ≥2023.07 (pas de board mochabin).
|
||||||
|
Branche `feature/748-enhanced-tow-boot-http-netboot-serial-fl` parkée (spec+CI+Kconfig en
|
||||||
|
place, dépend du backport wget OU port board mainline).
|
||||||
|
|
||||||
|
### ⬜ Rig netboot temporaire gk2 à démonter (quand c3box autonome)
|
||||||
|
|
||||||
|
- `lan1=192.168.77.1/24` avec dnsmasq DHCP + `nft iif lan1 accept` + nginx `:8099` encore actifs.
|
||||||
|
- À retirer une fois c3box en prod (voir TODO T5 — teardown rig).
|
||||||
|
|
||||||
|
### ⬜ Bootloader propre à faire (#748 ou alternative)
|
||||||
|
|
||||||
|
- boot.scr = workaround ; fix durable = enhanced Tow-Boot (#748, bloqué ciseau) OU corriger
|
||||||
|
les adresses de boot dans l'image (extlinux.conf → `0x0a000000`). Voir TODO T5.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ 2026-06-22 : triage issues (30 ouvertes → revue obsolètes)
|
||||||
|
|
||||||
|
- **Fermées (user-validé 2026-06-22)** : #722 (nDPId — décidé contre, reverté) ·
|
||||||
|
#475 ToolBoX Phase 1 (live 2.7.x) · #502/#507/#508 Social mapping (carto +
|
||||||
|
/social/me + report PDF live) · #495 Phase 5 mitm-LXC (superseded par #662 Go
|
||||||
|
sbxmitm host) · #531 APK one-tap (superseded par #685/#686 non-root) ·
|
||||||
|
#486 geoip/ASN+flags+catégories dans rapports (livré master : geo.py + dpi_class.py +
|
||||||
|
report wiring ; complémentaire de #718 ASN collector ; worktree stale nettoyé) ·
|
||||||
|
#515 CDN detection (live `social_host_meta.cdn_vendor`) · #516 anti-bot detection
|
||||||
|
(live via #564/#565) · #519 enforcement plane (livré + **réparé** : blacklist-sync
|
||||||
|
avortait NXDOMAIN + timeout unit → fix `|| true` + TimeoutStartSec 600, vérifié live,
|
||||||
|
default-off ; inclut #522). Toolbox source bumpé 2.7.18 (fix live-patché sur gk2) ·
|
||||||
|
#468 /etc/secubox traversal (source+live = 0755, secrets/CA enfants restent 0750).
|
||||||
|
- **Actives (worktrees en cours)** : #655 webext banner · #615 security-posture ·
|
||||||
|
#494 secubox-core ExecStart · #498 Phase 7 WAF enforcement · #485 SOC scoring.
|
||||||
|
|
||||||
|
### 🔎 Reco T0 — recon live gk2 2026-06-24 (avant fix)
|
||||||
|
- ✅ **#494** : **FIX SYSTÉMIQUE poussé** (`fix/494-…`). Pas que core : 7 units re-chownaient
|
||||||
|
le parent partagé `/run/secubox` (core+hub services, eye-remote/eye-square/metablogizer/
|
||||||
|
metrics/p2p postinsts ; eye-square chownait aussi /var/log/secubox = pire). Tous nettoyés
|
||||||
|
(mkdir fallback only ; logs modules en sous-dossier propre ; orphan /etc/tmpfiles.d nettoyé).
|
||||||
|
**Vérifié live** : /run/secubox 1777 **root:root** stable après restart core ET hub ; webui 200.
|
||||||
|
Bumps core 1.1.7/hub 1.4.4/eye-remote 1.0.1/eye-square 1.0.4/metablog 1.2.2/metrics 1.0.4/p2p 1.7.1.
|
||||||
|
- ✅ **#471** (mesh /run/secubox) : déjà résolu (changelog mesh "drop install -d /run/secubox") → verify-close.
|
||||||
|
- ⬜ **#421** : sockets cachés en mount-ns privé (RuntimeDirectory) — mécanisme distinct, non traité.
|
||||||
|
- 🆕 Suivi (classe #511) : mesh/toolbox/admin font `install -d -o <module> /var/log/secubox`
|
||||||
|
(propriétaire du parent partagé = user module) → autres daemons ne peuvent créer leurs logs.
|
||||||
|
Séparé de #494, à traiter (sous-dossiers propres comme fait pour eye-square/p2p).
|
||||||
|
- **#447** : pas une fuite — `password_hash=null` → lockout kiosk + user CI parasite ;
|
||||||
|
**CI-image-gated** (rpi400, pas gk2).
|
||||||
|
- **#91** : `haproxy.cfg` active valide ; backup `*.broken-by-haproxyctl-*` prouve le bug
|
||||||
|
passé ; drift-guard #627 rattrape. Root cause = generate `haproxyctl` (api/main.py l.846/896).
|
||||||
|
- ✅ **#53** : **FIX poussé** (`fix/53-…`) — gate `ConditionPathExists=/var/ossec/etc/ossec.conf`
|
||||||
|
+ `RestartSec=5` ; module conservé (SIEM opt-in). Vérifié gk2 (/var/ossec absent). Bump 1.0.1.
|
||||||
|
- ✅ **#65** : déjà résolu en prod (webui.conf déployé inclut `secubox-routes.d/*.conf`,
|
||||||
|
163 snippets). Template `common/nginx/webui.conf` (stale) synchronisé sur `feature/65-…`.
|
||||||
|
Reco fermer. Convention : `secubox-routes.d/`=actif, `secubox.d/`=legacy.
|
||||||
|
- ✅ **#121** : **FIX poussé** (`fix/121-…`) — helper `fix_perms` chown -R secubox:secubox
|
||||||
|
le site dir après chaque ingest .git (metablog-ingest-site.sh). Script dev, pas de deploy.
|
||||||
|
- ⬜ Restent : **#91** (deploy WAF risqué) · **#65** (refactor include, risque 502) ·
|
||||||
|
**#447** (CI kiosk) · **#494/#471/#421** (worktree fix/494). Build+deploy toolbox 2.7.18 (#519) en attente.
|
||||||
|
- **Backlog/future** : #685/#686 APK non-root (plan verrouillé) · #592 webmail-hub ·
|
||||||
|
#514/#515/#516/#519/#522/#525 Phase 12-14 (#515 CDN / #516 anti-bot partiellement
|
||||||
|
couverts par antibot_sites/opgrade_sites du social graph) · #500 Utiq · #497/#480/
|
||||||
|
#478 VILLAGE3B Eye/poster · #472/#430/#429 Nextcloud · #471/#468/#421 perms (à
|
||||||
|
vérifier si déjà corrigées) · #467/#462/#460/#255/#254 hardware/kernel · #455 egress ·
|
||||||
|
#454/#453/#452/#449 mesh/BLE · #448/#447/#446/#434 kiosk · #422 vm cascade ·
|
||||||
|
#393/#379/#347 packaging · #513 WebUI sub-tabs.
|
||||||
|
- ⚠️ Fermeture finale = **user only** (sauf issues créées en session) ; les
|
||||||
|
recommandations ci-dessus sont commentées sur chaque issue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 2026-06-22 : DPI exfil + Netrunner report + sbxmitm fixes (tous mergés, live gk2)
|
||||||
|
|
||||||
|
Session livrée intégralement sur master + déployée. Détail dans HISTORY 2026-06-22.
|
||||||
|
|
||||||
|
### ✅ Fait (mergé + live)
|
||||||
|
- **DPI exfil pipeline (#687)** — `secubox-dpi 1.1.2` : flowcap (ndpiReader) → Go
|
||||||
|
collector (catégories cloud/media/game/adult/ai/messaging/filehost/social + scénarios
|
||||||
|
exfil) → `/api/v1/dpi/exfil` ; dashboard "Cloud Exfiltration Watch" + cartes repointées ;
|
||||||
|
beaconing tuné (#692) ; cumulatif 7j `cumulative.json` (#705) ; packagé arm64.
|
||||||
|
- **Report kbin = fiche Netrunner (#707)** — HTML (onglets Pistage/DPI/Overall + persona
|
||||||
|
néon) **et** PDF (`_persona_block` + "En un coup d'œil" + grille donuts + carto + tables
|
||||||
|
emoji). Charts en **PNG matplotlib** (#714, rendu universel iOS/Chrome) ; grille = une
|
||||||
|
image 2×2 (#716, fin des 24 pages). Classe via UA live + niveau R3 auto (wg peer).
|
||||||
|
- **sbxmitm** — cert forgé 24h→365d (#689, fin des "certificat expiré") ; fin de la
|
||||||
|
troncature >8MiB (#697, Gmail OK) ; splice own-domain **rejeté** (#688, on intercepte tout).
|
||||||
|
|
||||||
|
### ⬜ Next Up (différé)
|
||||||
|
- **#685/#686 APK on-device — NON-ROOT ONLY (plan verrouillé)** : VpnService in-app
|
||||||
|
(wireguard-go), CA en DER + network-security-config WebView, retrait du chemin root.
|
||||||
|
Gros build Android (CI + test device) → session dédiée. Détail : commentaire #685 + TODO.
|
||||||
|
- **DPI Phase 3** — ✅ enrichissement ASN (#719, 1.1.3) · ✅ historique + timeline
|
||||||
|
(#721, 1.1.4) · ❌ démon nDPId **écarté** (#722/#723 revertés) : risque perf
|
||||||
|
(démon permanent vs fenêtres ndpiReader bornées) sur board saturée → **on garde
|
||||||
|
ndpiReader**. **Phase 3 close.**
|
||||||
|
- **#685 APK on-device** — install auto CA + handoff WG + détection tunnel (en attente
|
||||||
|
décision rooted vs non-root du user).
|
||||||
|
- **Cosmétique PDF** — glyphes drapeaux régionaux dégradent en lettres (police embarquée) ;
|
||||||
|
chiffres légèrement espacés dans certaines cellules. Non bloquant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2026-06-19 : kbin Tor egress (#683) — ToolBoX 2.7.1, implémenté DARK
|
||||||
|
|
||||||
|
Switch + tunnel Tor quick-switch livrés sur `feature/683`, **défaut OFF / fail-closed**.
|
||||||
|
Détail dans la section "Implémenté DARK" ci-dessous + HISTORY 2026-06-19.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2026-06-19 : kbin milestone — ToolBoX 2.7.0 + chapitre Tor (plan)
|
||||||
|
|
||||||
|
Checkpoint de fin de session. Pas de changement de comportement runtime — docs +
|
||||||
|
positionnement + version + plan de la lame suivante.
|
||||||
|
|
||||||
|
- ✅ **ToolBoX 2.7.0** (middle release) — clôt la ligne 2.6.x (ad-intelligence /
|
||||||
|
Anti-Track v2 / anti-bot uTLS #662), ouvre le chapitre kbin « premier outil du
|
||||||
|
couteau suisse cyber ». kbin = perf transparente + full encrypted + poison/smog +
|
||||||
|
bandeau anti-adware + safe browsing.
|
||||||
|
- ✅ **Docs kbin** — wiki [`Kbin-Toolbox.md`](../docs/wiki/Kbin-Toolbox.md),
|
||||||
|
[`FAQ-KBIN-TOR.md`](../docs/FAQ-KBIN-TOR.md), blurb README.
|
||||||
|
- ✅ **Plan #683** — spec
|
||||||
|
[`2026-06-19-kbin-tor-anonymized-surfing-design.md`](../docs/superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md) :
|
||||||
|
endpoint Tor quick-switch (egress sortant, fail-closed, opt-in, no DNS leak,
|
||||||
|
inspection préservée). Dépend du cœur Go #662.
|
||||||
|
|
||||||
|
### ✅ Implémenté DARK — chapitre Tor (#683, ToolBoX 2.7.1, branche feature/683)
|
||||||
|
|
||||||
|
- ✅ **Transport tranché** : *torify l'egress MITM* (owner-match nft sur l'uid
|
||||||
|
`secubox-toolbox`/mitm-wg → Tor TransPort 9040 / DNSPort 5353). Inspection
|
||||||
|
préservée. Décision USER (vs dialer SOCKS5 #662 = bloqué, vs torify client = casse
|
||||||
|
l'inspection).
|
||||||
|
- ✅ **Switch** : flags `tor_mode`/`tor_preset` (filters.json) ; API kbin-gated
|
||||||
|
`GET/POST /admin/tor/{state,on,off,newnym,check-leaks}` ; onglet 🧅 WebUI (badge,
|
||||||
|
toggle, NEWNYM, sonde fuite). `tor_ctl.py` réutilise le control-port de secubox-tor.
|
||||||
|
- ✅ **Tunnel** : `conf/nft-toolbox-tor.nft` (fail-closed kill-switch + drop v6) +
|
||||||
|
`conf/torrc-toolbox-egress.conf` + reconciler root path-triggered
|
||||||
|
(`secubox-toolbox-tor.path` surveille filters.json → portail reste
|
||||||
|
NoNewPrivileges=true). nft chargé AVANT tor (pas de fenêtre clearnet).
|
||||||
|
- ✅ 166 tests verts ; license headers OK ; changelog 2.7.1.
|
||||||
|
|
||||||
|
#### ⬜ Avant flip ON (USER)
|
||||||
|
|
||||||
|
- Soak DARK puis `tor_mode=true` via l'onglet (admin.gk2).
|
||||||
|
- Test de fuite **hors-board** : l'IP réelle de la box ne doit jamais apparaître.
|
||||||
|
- Forcer `tls_splice` (#649) OFF quand armé (sinon flux asset fuient l'IP réelle).
|
||||||
|
- **Per-client (WG-hash)** : nécessite le dialer SOCKS5 du cœur Go #662 (l'owner-match
|
||||||
|
est global). Suivi sous #662.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2026-06-17/18 : Anti-Track v2 + perf/ops sprint (gk2 live)
|
||||||
|
|
||||||
|
Tout mergé sur master + déployé sur gk2. Détail dans HISTORY 2026-06-18.
|
||||||
|
|
||||||
|
- ✅ **Anti-Track v2 (#633, PR #637)** — bloque/empoisonne/anonymise, moteur
|
||||||
|
`privacy.py` + addon `privacy_guard.py`, learning (`learn.py`), IP-drop +
|
||||||
|
unbound DNS-refuse (`ip_dns.py`/`escalate.py`), bypass-seed + #filtres badges,
|
||||||
|
#social top-5. **Tourne DARK** (`privacy_enforce` unset). Wiki `Anti-Track.md`.
|
||||||
|
- ✅ **Banner saga (#636/#639, PR #638/#640)** — mitm sert loader/bundle pour
|
||||||
|
toute origine (PeerTube fixé), CSP fallback, top-bar, 1 bannière/visite.
|
||||||
|
- ✅ **#634/#635** — reset-all clients + emojis device/flag/hosting.
|
||||||
|
- ✅ **#642 (PR #643)** — social-graph ignore les edges IP-littéraux ; KPI
|
||||||
|
"Trackers vus" = table.
|
||||||
|
- ✅ **#644 (PR #645)** — hub dashboard/health-batch servis depuis cache TTL
|
||||||
|
(health-batch 3.3 s → 8 ms) ; clients/rich enrichit 12 max. **hub 1.4.6**.
|
||||||
|
- ✅ **#646 (PR #647)** — adaptive Accept-Encoding strip : plus de pages
|
||||||
|
CSP-strict tirées décompressées via le worker R3 GIL-bound. **toolbox 2.6.53**.
|
||||||
|
- ✅ **crowdsec** réparé (403 transitoire CDN → `dpkg --configure` RC=0, audit clean).
|
||||||
|
|
||||||
|
- ✅ **#623 (PR #648, merged 9950e9ec)** — clobber systémique RÉSOLU au source.
|
||||||
|
La vraie cause : boilerplate scaffold `install -d -m 750 /var/lib/secubox` +
|
||||||
|
`/run/secubox` (parents NUS) dans ~56 postinsts — écrit `-m 750` (3 chiffres),
|
||||||
|
d'où le ratage des sweeps précédents. Empiriquement prouvé que le form
|
||||||
|
`install -d -m 750 /parent/leaf` NE clobbe PAS le parent (seuls les targets
|
||||||
|
parents-nus). Fix : tous → 1777 (/run) / 0755 ; 6 lignes multi-arg splittées
|
||||||
|
(4 mettaient /var/lib en world-writable 1777) ; 3 `chmod 750 /var/log` ;
|
||||||
|
scaffold `new-package.sh` + `PATTERNS.md` ; core 1.1.8 tmpfiles.d déclare les 5
|
||||||
|
parents 0755. **PAS de mass-deploy** (60 paquets = mass-restart = risque
|
||||||
|
thundering-herd) ; live couvert par `dirs-guard.timer` ; arrive au prochain
|
||||||
|
build CI / reflash.
|
||||||
|
|
||||||
|
- ✅ **#649 Lever A — selective SNI-splice (PR #650, toolbox 2.6.54 LIVE dark)**.
|
||||||
|
New `tls_splice` addon (first in mitm-wg chain) splices pure-asset flows at the
|
||||||
|
TLS ClientHello — curated media seed (googlevideo/ytimg/fbcdn/twimg/scdn…) ∪
|
||||||
|
autolearn-promoted never-HTML hosts — so GIL-bound R3 workers skip
|
||||||
|
forge/decrypt/parse/16-addons on no-L7-value flows. Ships `tls_splice=observe`
|
||||||
|
(DARK: classify+log, still MITM). Deployed gk2, addon loads clean, 0 runtime
|
||||||
|
errors. Answer to "do we need full mitm?": YES for outbound HTTPS (per-host cert
|
||||||
|
forging is intrinsic) — but only decrypt what we modify. Lever B (Go/Rust core)
|
||||||
|
= strategic follow-up. WAF = later.
|
||||||
|
|
||||||
|
### ⬜ Next Up
|
||||||
|
|
||||||
|
- **#649 SOAK → FLIP** — review `would-splice` logs + `/run/secubox/splice.json`
|
||||||
|
on real traffic for a soak window, confirm no first-party/HTML host is
|
||||||
|
classified, then flip `tls_splice=on` in `/etc/secubox/toolbox/filters.json`
|
||||||
|
(hot-reload). Before flip: the fortknox-via-WebUI refresh gap is already fixed.
|
||||||
|
- **Lever B (#649 follow-up)** — Go/Rust forging-proxy core if A isn't enough.
|
||||||
|
- **Anti-Track v2 ARMING** (décision USER, gated) — soak observe-only puis flip
|
||||||
|
`privacy_enforce=true` ; régénérer `data/cdn-allowlist.txt` depuis les plages
|
||||||
|
publiques avant `privacy_ip_drop` ; `unbound-checkconf` avant `privacy_dns_feed`.
|
||||||
|
- **Tunnel R3 perf** — l'encoding fix aide ; reste la contention CPU board-wide
|
||||||
|
(load ~5/4 cœurs, workers mono-thread). Lever suivant = réduire les co-tenants
|
||||||
|
(gitea/R2-mitm/crowdsec/metrics) ou isoler le mitm, pas du tuning d'addon.
|
||||||
|
- **#615** — Security Posture dans la navbar du Hub (petit enhancement).
|
||||||
|
- **#592 webmail-hub** — BLOQUÉ : besoin client OAuth Google + vhost ; Phase 1
|
||||||
|
IMAP (Gandi/OVH) peut démarrer sans OAuth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2026-06-14 : ToolBoX privacy/perf sprint — 2.6.36 live (see HISTORY)
|
||||||
|
|
||||||
|
Tout mergé + déployé sur gk2 (kbin sain, `secubox-toolbox 2.6.36`).
|
||||||
|
Détail complet dans HISTORY 2026-06-14. Résumé :
|
||||||
|
|
||||||
|
- ✅ Protective spoof (#560), modular filters + ad-ghoster (#566, collapse
|
||||||
|
#584), media cache opt-in (#577), autolearn (#589/#591), DPI media donut
|
||||||
|
(#570), donut + domain-nugget cartographie (#553/#587, IP cachées #575,
|
||||||
|
favicons #555), guirlande banner + pin (#572/#578), webext popup panel
|
||||||
|
(#574), /ca/fingerprint R3 (#562), postinst restart fix (#581),
|
||||||
|
detect_antibot deployment-vs-challenge (#564).
|
||||||
|
- ✅ Clients : APK v0.3.0 (zero-tap launch+boot), webext v0.1.4.
|
||||||
|
- ✅ Fixes live : Nextcloud iPhone photos (files_antivirus off + PHP
|
||||||
|
limits), kbin 503 (#581).
|
||||||
|
|
||||||
|
### ⬜ Next Up
|
||||||
|
|
||||||
|
- **#592 secubox-webmail-hub** (Gmail OAuth2 + Gandi + OVH, inbox unifié) —
|
||||||
|
design filé, **BLOQUÉ** : besoin d'un client OAuth Google (client_id/
|
||||||
|
secret/redirect) + nom de vhost + (read-only Phase 1 ?). Phase 1 IMAP
|
||||||
|
(Gandi/OVH) peut démarrer sans OAuth sur "start phase 1".
|
||||||
|
- Côté user : re-trust R3 CA `D5:E4:3A` sur l'iPhone (bannière HTTPS) ;
|
||||||
|
tester l'upload photo Nextcloud ; activer `media_cache` si voulu
|
||||||
|
(`/admin/filters/ui`) et surveiller `/admin/cache`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2026-06-13 : Browser extension — emancipate cartographie live (#532)
|
||||||
|
|
||||||
|
Extension navigateur (`clients/webext-toolbox/`, MV3 Firefox `.xpi` +
|
||||||
|
Chromium) sœur de l'app Android. Sort la *cartographie sociale* R3 dans
|
||||||
|
le navigateur : badge live des traceurs + popup.
|
||||||
|
|
||||||
|
- **Extension** : `manifest.json` (MV3, background `service_worker` +
|
||||||
|
`scripts` pour FF115+/Chromium), `api.js` (client `/wg/r3-check`,
|
||||||
|
`/social/me` → token, `/social/graph/{token}`, `/social/wipe`),
|
||||||
|
`background.js` (badge = total_trackers, re-pair silencieux si token
|
||||||
|
expiré, couleur escalade gold→anti-bot→opérateur), popup (4 tuiles
|
||||||
|
stats + **mini Round-Eye graph SVG sans dépendance** + top-traceurs
|
||||||
|
taggés CDN/anti-bot/opérateur + actions cartographie/PDF/RGPD-wipe),
|
||||||
|
options (hôte/fenêtre/token manuel). Pas de CORS backend nécessaire
|
||||||
|
(host_permissions). Validé : JSON+JS+SVG OK, `.xpi` build 11.8 KB.
|
||||||
|
- **Serve depuis la toolbox** (`2.6.14`) : `GET /wg/toolbox.xpi` (local
|
||||||
|
sinon 302 → release), bouton `🧩 Extension navigateur` sur les 2
|
||||||
|
panneaux onboard, helper `secubox-toolbox-fetch-xpi`, postinst dir.
|
||||||
|
- **CI** : `build-webext.yml` — `web-ext lint` (0 erreur, 2 warnings
|
||||||
|
bénins) + build, artifact, release asset sur tag `webext-v*`.
|
||||||
|
- **Release** (PR #540 + #541, mergées) : tag `webext-v0.1.1` poussé →
|
||||||
|
CI a publié `secubox-toolbox-webext.xpi` (téléchargeable, vérifié 200).
|
||||||
|
`make_latest:false` + URL **tag-pinned** dans `/wg/toolbox.xpi` +
|
||||||
|
`secubox-toolbox-fetch-xpi` pour ne pas voler le pointeur "Latest" à la
|
||||||
|
release APK Android (dont l'endpoint résout via `/releases/latest/...`).
|
||||||
|
→ bumper le tag dans la constante + le helper à chaque `webext-v*`.
|
||||||
|
- **Reste à faire** : signature AMO (`.xpi` non signé = sideload/dev) ;
|
||||||
|
endpoint SSE `/social/live/{token}` optionnel ; icône PNG Chromium ;
|
||||||
|
contrôle Poke/Emancipate par-site quand #525 (déception) arrive ;
|
||||||
|
déployer `secubox-toolbox 2.6.14` sur la board pour activer le bouton.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2026-06-13 : Android ToolBox app — serve + root-mode silent onboarding (#531/#536/#538)
|
||||||
|
|
||||||
|
App compagnon Android **one-tap R3** pour la cabine VILLAGE3B
|
||||||
|
(`clients/android-toolbox/`, `in.secubox.toolbox`, Kotlin + Compose).
|
||||||
|
|
||||||
|
- **#531 — scaffold + CI** : projet Gradle/Compose (5-step stepper
|
||||||
|
Discover→InstallCa→ImportProfile→Verify→Done), client `HttpURLConnection`,
|
||||||
|
workflow `build-android-apk.yml` (debug APK artifact, release asset sur
|
||||||
|
tag `android-v*`). CI **GREEN**.
|
||||||
|
- **#536 — serve depuis la toolbox** : endpoint `GET /wg/toolbox.apk`
|
||||||
|
(sert le build local `/var/lib/secubox/toolbox/android/`, sinon 302 →
|
||||||
|
release GitHub) + bouton *📱 Installer l'app ToolBoX (1-tap)* dans les
|
||||||
|
panneaux onboard kbin + helper `secubox-toolbox-fetch-apk`. Vérifié :
|
||||||
|
200 `application/vnd.android.package-archive`, 14.8 MB.
|
||||||
|
- **#538 — root-mode silent onboarding** (PR #539, branche poussée) :
|
||||||
|
bouton *⚡ Installation automatique (root)* sur devices rootés →
|
||||||
|
install CA dans le magasin **système** (bind-mount cacerts + APEX
|
||||||
|
conscrypt, SELinux ctx, `subject_hash_old` en Kotlin pur) + tunnel
|
||||||
|
WireGuard **natif noyau** (`ip link add … type wireguard` + `wg set`) +
|
||||||
|
vérif R3 auto. Fallback handoff app WireGuard si noyau sans WG. Toutes
|
||||||
|
les actions root gated derrière le tap explicite. Nouveaux fichiers
|
||||||
|
`RootShell.kt`, `RootOnboard.kt`, step `RootAuto` (log streamé).
|
||||||
|
- **Reste à faire** : release signing (keystore secret CI) pour une
|
||||||
|
empreinte publiée stable — actuellement debug-signé (sideload).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2026-06-12 : Admin WireGuard tunnel + SSH hardening (ref #529)
|
||||||
|
|
||||||
|
Accès admin out-of-band + fermeture de la surface SSH publique.
|
||||||
|
|
||||||
|
- **wg-admin** (#529) : interface dédiée UDP **51821**, server `10.98.0.1/24`,
|
||||||
|
distincte de wg-toolbox (51820). Peer `gandalf-admin` @ `10.98.0.2`.
|
||||||
|
nft drop-in `secubox-admin-wg.nft` (udp/51821), `wg-quick@wg-admin`
|
||||||
|
enabled. Client importé dans NetworkManager du poste dev, tunnel UP,
|
||||||
|
`ssh root@10.98.0.1` confirmé (key auth).
|
||||||
|
- **Découverte sécurité** : la box subissait un brute-force SSH public
|
||||||
|
actif (centaines de tentatives 87.251.64.x / 51.68.34.x + IPs déjà
|
||||||
|
blacklistées). Le routeur `192.168.1.254` port-forward :22 → box sur
|
||||||
|
`eth1`/`lan0`, et l'input chain a un blanket `iif eth1 accept` (le
|
||||||
|
DNAT préserve l'IP source publique réelle).
|
||||||
|
- **Hardening appliqué + vérifié** :
|
||||||
|
- sshd : `PasswordAuthentication no` + `PermitRootLogin prohibit-password`
|
||||||
|
(drop-in `99-secubox-hardening.conf`, key-only).
|
||||||
|
- nft SSH-guard : `tcp dport 22 ip saddr != { 192.168.1.0/24, 10.0.0.0/8 } drop`
|
||||||
|
inséré AVANT `iif eth1 accept` (live sans flush + persisté dans
|
||||||
|
`/etc/nftables.conf`).
|
||||||
|
- Résultat : `ssh root@10.98.0.1` (tunnel) OK key-only ; public
|
||||||
|
`admin.gk2.secubox.in:22` **timeout (bloqué)**. Tables blacklist/wg
|
||||||
|
intactes.
|
||||||
|
- **Script reproductible** `scripts/setup-admin-tunnel.sh`
|
||||||
|
(`provision | add <name> | harden`), idempotent, branche `feature/529`
|
||||||
|
poussée (pas de PR).
|
||||||
|
- **Reste à faire (côté user)** : retirer le port-forward :22 du routeur
|
||||||
|
(le tunnel remplace l'accès) ; IPv6 SSH non couvert par le guard v4
|
||||||
|
(à ajouter si exposition IPv6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2026-06-11 : Phase 12.C + Phase 13 COMPLETE (protection enforcement plane) — v2.13.16→19 (ref #518-#528)
|
||||||
|
|
||||||
|
### ✅ Phase 12.C — operator-grade / state-adjacent (#518, v2.13.16)
|
||||||
|
detect_operator_grade : telco header-enrichment (MSISDN/x-acr), consortium
|
||||||
|
(Utiq/TrustPid), data-broker / state-adjacent (LiveRamp/BlueKai/Acxiom/
|
||||||
|
Neustar/Tapad/Experian/Palantir-class). Top-severity void-purple lens +
|
||||||
|
PDF section. `secubox-toolbox 2.6.7`.
|
||||||
|
|
||||||
|
### ✅ Phase 13 — protection enforcement plane (#519) COMPLETE
|
||||||
|
Le plan de bannissement (Vortex DNS + WAF + CrowdSec) enforce maintenant
|
||||||
|
sur le browsing des appareils, à tous les niveaux egress.
|
||||||
|
|
||||||
|
| Track | Issue | Livré | Tag |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 13.A spine | #521 | nft set `inet secubox_blacklist` + forward-drop chain ; sync CrowdSec+threat-intel | v2.13.17 (2.6.8) |
|
||||||
|
| 13.B DNS-guard | #522 | résout domaines blocklistés → IPs (anti-DoH bypass) + détection DoH/DoT count-only | v2.13.17 (2.6.9) |
|
||||||
|
| 13.C attribution | #524 | per-device (WG/lease hash) blocked-attempts + quarantine set + endpoints | v2.13.18 (2.6.10) |
|
||||||
|
| 13.D feedback | #527 | escalation evaluator (detections→nft/cscli/quarantine), audit-log, **default OFF** | v2.13.19 (2.6.11) |
|
||||||
|
|
||||||
|
**Doctrine** : DEFAULT DROP préservé (policy accept n'ajoute que des drops) ;
|
||||||
|
pas de WAF bypass ; anonyme (mac_hash sel rotatif) ; tout réversible (TTL +
|
||||||
|
unban) ; escalade opt-in par source.
|
||||||
|
|
||||||
|
**Bug latent corrigé (#521)** : `override_dh_strip` ne tourne jamais pour
|
||||||
|
un paquet `Architecture: all` → tous les drop-ins nft/unbound/nginx/perf
|
||||||
|
avaient cessé de shipper (cause racine de la live-config-drift). Déplacé
|
||||||
|
vers `execute_after_dh_auto_install`. Mémoire ajoutée.
|
||||||
|
|
||||||
|
### 💡 Idée future capturée (#525)
|
||||||
|
Phase 14 « plan de déception » : au lieu de bloquer les IPs trackers,
|
||||||
|
générer des pseudo-réponses proxy (indistinguable du drop, pollue le
|
||||||
|
profil) ; idem neutraliser les scripts CDN préchargés. Pour plus tard.
|
||||||
|
|
||||||
|
### 🧹 État du dépôt
|
||||||
|
Toutes les branches Phase 11/12/13 mergées + supprimées sur origin.
|
||||||
|
master @ `v2.13.19` (`secubox-toolbox 2.6.11`). Worktrees Phase 11-13
|
||||||
|
nettoyés. (Worktrees plus anciens #429/#485/#486/#490/#494/#495/#498 +
|
||||||
|
license = travail parallèle, non touchés.)
|
||||||
|
|
||||||
|
### ⬜ Next up
|
||||||
|
- **Phase 13 opt-in tuning** : activer les sources d'escalade (env
|
||||||
|
`SECUBOX_ESCALATE_*`) selon politique opérateur quand voulu.
|
||||||
|
- **threatfox feed = 0 IOCs** : investiguer pourquoi l'ingestion domain
|
||||||
|
est vide (impacte 13.B resolved_domains).
|
||||||
|
- **Phase 14 déception** (#525) quand prêt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2026-06-10 (soir) : Phase 11 COMPLETE + Phase 12.A/B + toolbox tabs — merged v2.13.15 (ref #502-#516)
|
||||||
|
|
||||||
|
Tout le stack Phase 11 + Phase 12.A/B mergé en une PR consolidée (#517),
|
||||||
|
`secubox-toolbox 2.5.2 → 2.6.6`, tag **v2.13.15**.
|
||||||
|
|
||||||
|
### ✅ Done — mergé master
|
||||||
|
|
||||||
|
| Issue | Livré | Version |
|
||||||
|
|---|---|---|
|
||||||
|
| #505 11.A backend | correlation engine + SQLite + /social API | 2.6.0 |
|
||||||
|
| #507 11.B frontend | d3 graph + i18n FR/EN + favicon proxy + wipe + /social/me | 2.6.1 |
|
||||||
|
| #513 toolbox tabs | 5-tab WebUI, kbin /admin/ inline UI supprimé | 2.6.2 |
|
||||||
|
| #508 11.C evidence | consent-probe + PDF bilingue FR/EN | 2.6.3 |
|
||||||
|
| #515 12.A CDN | detect_cdn + Round-Eye central-hotspot graph | 2.6.4 |
|
||||||
|
| #516 12.B anti-bot | detect_antibot + ring levels visibles + Carto/Reset | 2.6.5/2.6.6 |
|
||||||
|
|
||||||
|
### ✅ Phase 11 — social mapping per device (#502)
|
||||||
|
|
||||||
|
Live : `https://kbin.gk2.secubox.in/social/me` (🕸️ Ma carto).
|
||||||
|
Graphe Round-Eye : l'appareil = œil central pulsant, sites sur anneau
|
||||||
|
interne (forceRadial 0.9), trackers anneau externe. Montre le relais
|
||||||
|
ad-tech `35.214.136.108` reliant 360yield + seedtag + smartadserver +
|
||||||
|
smilewanted. PDF d'évidence bilingue (consentement RGPD art. 6.1.a+7,
|
||||||
|
transferts hors-UE art. 44). Effacement RGPD art. 17. Anonyme (mac_hash
|
||||||
|
sel rotatif 24h, aucune valeur cookie brute).
|
||||||
|
|
||||||
|
### ✅ Phase 12 — anti-human-detection platform (#514)
|
||||||
|
|
||||||
|
- **12.A CDN** : detect_cdn (Cloudflare/Fastly/Akamai/CloudFront/Google/
|
||||||
|
Vercel/Netlify/Bunny/KeyCDN/Sucuri/Imperva). Nodes teintés par vendor,
|
||||||
|
table social_host_meta, agrégat by_cdn.
|
||||||
|
- **12.B anti-bot** : detect_antibot (reCAPTCHA/hCaptcha/Turnstile/
|
||||||
|
Datadome/PerimeterX-HUMAN/Arkose/Kasada/Akamai-BotManager). Lens
|
||||||
|
cinnabar sévère + ring d'alerte + bannière "challenged your humanity".
|
||||||
|
Table social_antibot per-client, agrégat by_antibot.
|
||||||
|
**Bypass NON livré** — gated derrière doctrine (séquencement #514).
|
||||||
|
- **Ring levels visibles** : forceRadial dominant + guides d'anneau +
|
||||||
|
cache-bust ?v=264b (le user ne voyait pas la réorg ; corrigé).
|
||||||
|
- **Outils opérateur Clients tab** : 🕸️ Carto (ouvre le graphe client
|
||||||
|
via token, redirige vers kbin absolu), ↺ Reset/RAZ (efface social +
|
||||||
|
events + score).
|
||||||
|
|
||||||
|
### ⚠️ Round Eye gadget — diagnostic, fix physique requis
|
||||||
|
|
||||||
|
OTG USB CDC-Ethernet **TX queue wedged** (NETDEV WATCHDOG timeout sur
|
||||||
|
3-1.1.2). Le gadget énumère (descripteurs lisibles) mais le data path
|
||||||
|
TX est mort — control transfers OK, bulk KO. Recovery gk2 épuisée
|
||||||
|
(link bounce, unbind/rebind → probe error -110). **Power-cycle du Pi
|
||||||
|
Zero ou re-seat câble OTG nécessaire** ; self-heal au prochain boot
|
||||||
|
propre via la règle systemd .link.
|
||||||
|
|
||||||
|
### ⬜ Next up
|
||||||
|
|
||||||
|
- **Phase 12.C** (#514) : opérateur-grade / state-adjacent (étend #500
|
||||||
|
Utiq). Détection identité carrier-grade + analytics state-adjacent.
|
||||||
|
- **Phase 12.B bypass + 12.D noise** : derniers, chacun gated par sa
|
||||||
|
doctrine + design review (interférence active 3rd-party = R3 opt-in).
|
||||||
|
- **Round Eye** : reprendre côté gk2 dès que le Pi re-énumère propre.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 2026-06-10 : Phase 11 social mapping (A+B) + system triage round (ref #502-#509)
|
||||||
|
|
||||||
|
Grosse journée : Phase 11 social mapping shippé jusqu'au frontend live,
|
||||||
|
puis une cascade de fixes système découverts par l'utilisateur en
|
||||||
|
production sur gk2.
|
||||||
|
|
||||||
|
### ✅ Done — Phase 11 social mapping (#502 parent)
|
||||||
|
|
||||||
|
| Issue | Phase | État |
|
||||||
|
|---|---|---|
|
||||||
|
| #505 / PR #506 | **11.A backend** : correlation engine + SQLite + API | ✅ mergeable, déployé live `secubox-toolbox 2.6.0` |
|
||||||
|
| #507 | **11.B frontend** : d3 graph + i18n FR/EN + favicon proxy + wipe modal | ✅ déployé live `2.6.1`, branche poussée |
|
||||||
|
| #508 | **11.C evidence + PDF** | 🔄 WIP checkpoint `55626e51` (schema + GeoIP fold + evidence helper) |
|
||||||
|
|
||||||
|
**Design** : 2 rounds de design lock sur #502 (Gemini + GPT mockups),
|
||||||
|
edge-thickness + animated-pulse + tracker bottom-sheet + 3s wipe
|
||||||
|
countdown verrouillés.
|
||||||
|
|
||||||
|
**Live URL** : `https://kbin.gk2.secubox.in/social/me` (splash → 🕸️ Ma carto).
|
||||||
|
Le graphe montre les trackers cross-site réels (relais ad-tech
|
||||||
|
`35.214.136.108` reliant 360yield + seedtag + smartadserver + smilewanted).
|
||||||
|
|
||||||
|
**Fixes live-deploy critiques découverts** :
|
||||||
|
- `social_graph.py` : `from . import local_store` ne résolvait jamais
|
||||||
|
(mitmproxy charge les addons en top-level) → inliné le WG peer hash.
|
||||||
|
- **PYTHONPATH manquant dans le launcher mitm-wg** : TOUS les addons
|
||||||
|
(`inject_banner` dpi/geo/store, `social_graph`) avaient leurs
|
||||||
|
`from secubox_toolbox import …` silencieusement dégradés. Fix global.
|
||||||
|
- i18n déplacé de `data-*` attr vers `<script>` (apostrophes FR
|
||||||
|
cassaient `JSON.parse`).
|
||||||
|
- StaticFiles mount + chmod 0755 `/usr/share/secubox/www` (kbin passe
|
||||||
|
par HAProxy direct uvicorn, bypass nginx).
|
||||||
|
- d3 : full-viewport + pan/pinch-zoom + pre-warm 300 ticks + autoFit
|
||||||
|
data-based (146 nodes spread off-screen avant).
|
||||||
|
|
||||||
|
### ✅ Done — triage système gk2 (2026-06-10)
|
||||||
|
|
||||||
|
| Bug | Cause racine | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| CrowdSec firewall status faux | bouncer tournait mais sans tables nft (flush externe) | restart bouncer → `ip crowdsec` + `ip6 crowdsec6` recréées, 100 décisions live |
|
||||||
|
| WAF /threats + tracked attackers vides | `/var/log/secubox` 0750 secubox-toolbox bloquait traversal aggregator (user `secubox`) | chmod 0755 live |
|
||||||
|
| WAF /stats timeout 30s+ | `_get_threat_stats()` re-parsait 110 MB / 332k JSONL à CHAQUE requête (CPU 89%) | **#509 double-buffer cache** (disk + byte-position incrémental) `secubox-waf 1.2.2` |
|
||||||
|
| SOC /soc/ status WAF+firewall faux | consommait les mêmes endpoints WAF cassés | résolu en cascade par le fix WAF |
|
||||||
|
| PeerTube + PhotoPrism 502 | LXC STOPPED | `lxc-start` → 200 / 307 |
|
||||||
|
|
||||||
|
### ✅ Done — CI + release
|
||||||
|
|
||||||
|
- **#503 / PR #504** : drop espressobin-v7 + ultra du matrix build-image
|
||||||
|
scheduled (faisaient échouer le pipeline release v2.13.9-12).
|
||||||
|
- **#509 / PR #510** : double-buffer WAF cache.
|
||||||
|
- **Merge #504 + #510 → master** (`3ebb4477`, `a6f44807`).
|
||||||
|
- **Tag `v2.13.14`** poussé.
|
||||||
|
|
||||||
|
### ⬜ Next up
|
||||||
|
|
||||||
|
- **Round Eye gadget** : ne voit plus le lien gk2, montre ses métriques
|
||||||
|
locales. iface `eye-remote` UP côté gk2, route `/api/v1/eye-remote/*`
|
||||||
|
renvoie page erreur. Investigation côté Pi Zero nécessaire.
|
||||||
|
- **admin.gk2/toolbox/ tab** : le toolbox est DÉJÀ wiré (`/toolbox/`
|
||||||
|
alias + sidebar). User veut surfacer l'UI kbin/admin dedans —
|
||||||
|
décision en attente : proxy_pass / iframe / sous-tab.
|
||||||
|
- **Phase 11.C** : reprendre depuis `55626e51` (consent probe addon +
|
||||||
|
extra-EU flag + PDF bilingue + wire frontend).
|
||||||
|
- **Postinst patch** : `/var/log/secubox` 0755 en source (pour l'instant
|
||||||
|
fix live uniquement) — même pattern que `/etc/secubox` + `www`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
1
.github/workflows/build-all-live-usb.yml
vendored
|
|
@ -48,6 +48,7 @@ jobs:
|
||||||
output_pattern: "secubox-live-amd64-*.img*"
|
output_pattern: "secubox-live-amd64-*.img*"
|
||||||
needs_qemu: false
|
needs_qemu: false
|
||||||
embed_image: false
|
embed_image: false
|
||||||
|
extra_args: "--kiosk"
|
||||||
|
|
||||||
# MOCHAbin (arm64) - U-Boot distroboot
|
# MOCHAbin (arm64) - U-Boot distroboot
|
||||||
- platform: mochabin
|
- platform: mochabin
|
||||||
|
|
|
||||||
70
.github/workflows/build-android-apk.yml
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Build the SecuBox Android ToolBox client APK (#531).
|
||||||
|
# No Gradle wrapper jar is committed (text-only scaffold) — setup-gradle
|
||||||
|
# provides Gradle ; setup-android provides the SDK. Produces a debug APK
|
||||||
|
# artifact (sideloadable). Release signing is a follow-up (needs a
|
||||||
|
# keystore secret).
|
||||||
|
name: build-android-apk
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
paths: [ "clients/android-toolbox/**", ".github/workflows/build-android-apk.yml" ]
|
||||||
|
tags: [ "android-v*" ]
|
||||||
|
pull_request:
|
||||||
|
paths: [ "clients/android-toolbox/**" ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # needed to attach the APK to a release on tags
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: clients/android-toolbox
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "17"
|
||||||
|
|
||||||
|
- name: Set up Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
with:
|
||||||
|
gradle-version: "8.9"
|
||||||
|
|
||||||
|
- name: Build debug APK
|
||||||
|
run: gradle :app:assembleDebug --no-daemon --stacktrace
|
||||||
|
|
||||||
|
- name: Upload APK artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: secubox-toolbox-android-debug
|
||||||
|
path: clients/android-toolbox/app/build/outputs/apk/debug/*.apk
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
# On android-v* tags, publish the APK as a release asset under the
|
||||||
|
# stable name the toolbox fetch helper + /wg/toolbox.apk expect
|
||||||
|
# (#536). `latest/download/secubox-toolbox-android.apk` resolves to
|
||||||
|
# whichever release is newest.
|
||||||
|
- name: Stage release asset
|
||||||
|
if: startsWith(github.ref, 'refs/tags/android-v')
|
||||||
|
run: |
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE/release"
|
||||||
|
cp app/build/outputs/apk/debug/app-debug.apk \
|
||||||
|
"$GITHUB_WORKSPACE/release/secubox-toolbox-android.apk"
|
||||||
|
|
||||||
|
- name: Publish release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/android-v')
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: release/secubox-toolbox-android.apk
|
||||||
|
fail_on_unmatched_files: true
|
||||||
19
.github/workflows/build-image.yml
vendored
|
|
@ -24,8 +24,8 @@ on:
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- mochabin
|
- mochabin
|
||||||
- espressobin-v7
|
- espressobin-v7 # on-demand only — disabled in scheduled CI, ref #503
|
||||||
- espressobin-ultra
|
- espressobin-ultra # on-demand only — disabled in scheduled CI, ref #503
|
||||||
- vm-x64
|
- vm-x64
|
||||||
- vm-arm64
|
- vm-arm64
|
||||||
- rpi400
|
- rpi400
|
||||||
|
|
@ -50,7 +50,12 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# Handle all event types: push (tags), workflow_call, workflow_dispatch
|
# Handle all event types: push (tags), workflow_call, workflow_dispatch
|
||||||
board: ${{ (github.event_name == 'push' || (inputs.board == 'all' || inputs.board == '')) && fromJson('["mochabin","espressobin-v7","espressobin-ultra","vm-x64","rpi400"]') || (github.event.inputs.board == 'all' && fromJson('["mochabin","espressobin-v7","espressobin-ultra","vm-x64","rpi400"]') || fromJson(format('["{0}"]', inputs.board || github.event.inputs.board || 'vm-x64'))) }}
|
# Scheduled / tag-push matrix excludes espressobin-v7 + espressobin-ultra (#503) :
|
||||||
|
# those board builds fail in the cross-arm64 chroot stage and block the
|
||||||
|
# downstream release.yml job for every image even though fail-fast is off.
|
||||||
|
# Operators can still build them on-demand via workflow_dispatch (the
|
||||||
|
# choice list above retains the entries).
|
||||||
|
board: ${{ (github.event_name == 'push' || (inputs.board == 'all' || inputs.board == '')) && fromJson('["mochabin","vm-x64","rpi400"]') || (github.event.inputs.board == 'all' && fromJson('["mochabin","vm-x64","rpi400"]') || fromJson(format('["{0}"]', inputs.board || github.event.inputs.board || 'vm-x64'))) }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -221,15 +226,17 @@ jobs:
|
||||||
| Image | Board | Architecture | Description |
|
| Image | Board | Architecture | Description |
|
||||||
|-------|-------|--------------|-------------|
|
|-------|-------|--------------|-------------|
|
||||||
| `secubox-mochabin-bookworm.img.gz` | MOCHAbin | arm64 | Marvell Armada 7040 (Pro) |
|
| `secubox-mochabin-bookworm.img.gz` | MOCHAbin | arm64 | Marvell Armada 7040 (Pro) |
|
||||||
| `secubox-espressobin-v7-bookworm.img.gz` | ESPRESSObin v7 | arm64 | Marvell Armada 3720 (Lite) |
|
|
||||||
| `secubox-espressobin-ultra-bookworm.img.gz` | ESPRESSObin Ultra | arm64 | Marvell Armada 3720 (Lite+) |
|
|
||||||
| `secubox-rpi400-bookworm.img.gz` | Raspberry Pi 400 | arm64 | Pi 400 / Pi 4 |
|
| `secubox-rpi400-bookworm.img.gz` | Raspberry Pi 400 | arm64 | Pi 400 / Pi 4 |
|
||||||
| `secubox-vm-x64-bookworm.img.gz` | VirtualBox/QEMU | amd64 | VM for testing |
|
| `secubox-vm-x64-bookworm.img.gz` | VirtualBox/QEMU | amd64 | VM for testing |
|
||||||
| `create-qemu-arm64-vm.sh` | QEMU ARM64 | script | Run ARM64 on x86 hosts |
|
| `create-qemu-arm64-vm.sh` | QEMU ARM64 | script | Run ARM64 on x86 hosts |
|
||||||
|
|
||||||
|
*ESPRESSObin v7 and Ultra board images are no longer published in
|
||||||
|
scheduled releases (see #503). Board support remains in tree and
|
||||||
|
on-demand builds are available via workflow_dispatch.*
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
**ARM64 boards (MOCHAbin, ESPRESSObin):**
|
**ARM64 boards (MOCHAbin, Raspberry Pi 400):**
|
||||||
```bash
|
```bash
|
||||||
# Flash to SD card or eMMC
|
# Flash to SD card or eMMC
|
||||||
gunzip -c secubox-mochabin-bookworm.img.gz | sudo dd of=/dev/sdX bs=4M status=progress
|
gunzip -c secubox-mochabin-bookworm.img.gz | sudo dd of=/dev/sdX bs=4M status=progress
|
||||||
|
|
|
||||||
25
.github/workflows/build-packages.yml
vendored
|
|
@ -63,6 +63,11 @@ jobs:
|
||||||
# Build the flat {package, arch} matrix. Honour the workflow_dispatch
|
# Build the flat {package, arch} matrix. Honour the workflow_dispatch
|
||||||
# `arch` and `package` filters if set (empty on `push: tags` events).
|
# `arch` and `package` filters if set (empty on `push: tags` events).
|
||||||
requested_arch="${REQUESTED_ARCH:-}"
|
requested_arch="${REQUESTED_ARCH:-}"
|
||||||
|
# `both` means build every arch — same as the empty (push: tags)
|
||||||
|
# case. Without this the matrix filter (which only compares against
|
||||||
|
# amd64/arm64/empty) yields an EMPTY matrix, so no package builds and
|
||||||
|
# `collect` fails.
|
||||||
|
[ "$requested_arch" = "both" ] && requested_arch=""
|
||||||
requested_pkg="${REQUESTED_PKG:-}"
|
requested_pkg="${REQUESTED_PKG:-}"
|
||||||
|
|
||||||
combos=$(find packages/secubox-* -path "*/debian/control" -not -path "*/debian/*/DEBIAN/control" \
|
combos=$(find packages/secubox-* -path "*/debian/control" -not -path "*/debian/*/DEBIAN/control" \
|
||||||
|
|
@ -152,7 +157,12 @@ jobs:
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -y -qq \
|
sudo apt-get install -y -qq \
|
||||||
build-essential dpkg-dev debhelper devscripts fakeroot \
|
build-essential dpkg-dev debhelper devscripts fakeroot \
|
||||||
dh-python python3-all python3-setuptools
|
dh-python python3-all python3-setuptools golang-go
|
||||||
|
# golang-go satisfies Build-Depends of the pure-Go packages
|
||||||
|
# (secubox-dpi, secubox-toolbox-ng: CGO_ENABLED=0, GOARCH=arm64,
|
||||||
|
# -mod=vendor offline cross-compile). ubuntu-24.04 ships >= 1.22.
|
||||||
|
# Without it dpkg-checkbuilddeps aborts the arm64 build — this was
|
||||||
|
# the real cause of the "arm64 red" runs, not a CGO toolchain gap.
|
||||||
# arm64 cross-toolchain — dh_strip and dh_makeshlibs invoke
|
# arm64 cross-toolchain — dh_strip and dh_makeshlibs invoke
|
||||||
# aarch64-linux-gnu-{strip,objdump} when -a arm64 is passed.
|
# aarch64-linux-gnu-{strip,objdump} when -a arm64 is passed.
|
||||||
# Without these, arch-specific packages shipping prebuilt
|
# Without these, arch-specific packages shipping prebuilt
|
||||||
|
|
@ -213,7 +223,18 @@ jobs:
|
||||||
# no-op; for arm64 jobs that don't compile native code (Python +
|
# no-op; for arm64 jobs that don't compile native code (Python +
|
||||||
# prebuilt arm64 binaries — like sentinelle-gsm), -a arm64 is
|
# prebuilt arm64 binaries — like sentinelle-gsm), -a arm64 is
|
||||||
# enough to cross-stamp the .deb.
|
# enough to cross-stamp the .deb.
|
||||||
dpkg-buildpackage -us -uc -b -a ${{ matrix.arch }}
|
#
|
||||||
|
# Pure-Go packages (CGO_ENABLED=0, GOARCH cross) only need the `go`
|
||||||
|
# toolchain, which is present via golang-1.22-go. But their
|
||||||
|
# `Build-Depends: golang-go (>= 1.22)` trips dpkg-checkbuilddeps
|
||||||
|
# because apt registers golang-1.22-go, not the golang-go
|
||||||
|
# metapackage, on the runner. Skip the dep check (-d) for just these
|
||||||
|
# — the compiler is there and the build is self-contained (-mod=vendor).
|
||||||
|
DEPFLAG=""
|
||||||
|
case "${{ matrix.package }}" in
|
||||||
|
secubox-dpi|secubox-toolbox-ng|secubox-waf-ng) DEPFLAG="-d" ;;
|
||||||
|
esac
|
||||||
|
dpkg-buildpackage -us -uc -b $DEPFLAG -a ${{ matrix.arch }}
|
||||||
|
|
||||||
echo "✅ Build OK: ${{ matrix.package }} (${{ matrix.arch }})"
|
echo "✅ Build OK: ${{ matrix.package }} (${{ matrix.arch }})"
|
||||||
|
|
||||||
|
|
|
||||||
63
.github/workflows/build-webext.yml
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Build the SecuBox ToolBoX browser extension (#532).
|
||||||
|
# Plain JS/HTML/CSS — no bundler. web-ext lints + packages the .xpi.
|
||||||
|
# Produces an unsigned .xpi artifact; release signing (AMO) is a
|
||||||
|
# follow-up (needs AMO API credentials as secrets).
|
||||||
|
name: build-webext
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
paths: [ "clients/webext-toolbox/**", ".github/workflows/build-webext.yml" ]
|
||||||
|
tags: [ "webext-v*" ]
|
||||||
|
pull_request:
|
||||||
|
paths: [ "clients/webext-toolbox/**" ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # needed to attach the .xpi to a release on tags
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: clients/webext-toolbox
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
|
||||||
|
- name: Lint (web-ext)
|
||||||
|
run: npx --yes web-ext lint --source-dir . --self-hosted --ignore-files build.sh README.md
|
||||||
|
|
||||||
|
- name: Build .xpi (web-ext)
|
||||||
|
run: |
|
||||||
|
npx --yes web-ext build --source-dir . \
|
||||||
|
--artifacts-dir web-ext-artifacts --overwrite-dest \
|
||||||
|
--ignore-files build.sh README.md \
|
||||||
|
--filename "secubox-toolbox-webext.xpi"
|
||||||
|
|
||||||
|
- name: Upload .xpi artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: secubox-toolbox-webext
|
||||||
|
path: clients/webext-toolbox/web-ext-artifacts/secubox-toolbox-webext.xpi
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
# On webext-v* tags, publish the .xpi as a release asset under the
|
||||||
|
# stable name the toolbox fetch helper + /wg/toolbox.xpi expect.
|
||||||
|
# make_latest:false so this client release does NOT steal the
|
||||||
|
# "latest" pointer from the Android APK release (which the APK
|
||||||
|
# endpoint resolves via /releases/latest/download/…). The xpi
|
||||||
|
# endpoint/fetcher therefore use a tag-pinned download URL.
|
||||||
|
- name: Publish release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/webext-v')
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: clients/webext-toolbox/web-ext-artifacts/secubox-toolbox-webext.xpi
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
make_latest: false
|
||||||
48
README.md
|
|
@ -31,6 +31,54 @@
|
||||||
> - Press kit + candidatures France.gouv : [docs/marketing/PROMPT-claude-presse-gouv.md](docs/marketing/PROMPT-claude-presse-gouv.md)
|
> - Press kit + candidatures France.gouv : [docs/marketing/PROMPT-claude-presse-gouv.md](docs/marketing/PROMPT-claude-presse-gouv.md)
|
||||||
> - Issue tracking poster : [#497](https://github.com/CyberMind-FR/secubox-deb/issues/497)
|
> - Issue tracking poster : [#497](https://github.com/CyberMind-FR/secubox-deb/issues/497)
|
||||||
|
|
||||||
|
### 🕸️ Cartographie sociale — « You Have Been Tracked » (Phase 11)
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/assets/poster/kbin-you-have-been-tracked.png" alt="Poster YOU HAVE BEEN TRACKED — cartographie sociale kbin" width="500">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
> **Le même navigateur, reconnu de site en site.**
|
||||||
|
>
|
||||||
|
> En R3 consenti, la cabine corrèle les **cookies tiers** et les
|
||||||
|
> **fingerprints JA4** par device pour révéler, en temps réel, quels
|
||||||
|
> acteurs commerciaux reconnaissent votre navigateur à travers les sites
|
||||||
|
> visités. Un relais ad-tech reliant 4 éditeurs (360yield + seedtag +
|
||||||
|
> smartadserver + smilewanted via la même IP) saute aux yeux dans le
|
||||||
|
> graphe force-dirigé.
|
||||||
|
>
|
||||||
|
> - Vue per-client : `https://kbin.gk2.secubox.in/social/me` (🕸️ « Ma carto »)
|
||||||
|
> - Graphe d3 plein écran (pan / pinch-zoom), évidence cross-site,
|
||||||
|
> effacement RGPD art. 17, rapport PDF bilingue (Phase 11.C).
|
||||||
|
> - **Anonyme** : `mac_hash` à sel rotatif 24h, aucune valeur de cookie
|
||||||
|
> brute persistée. Tout calculé localement.
|
||||||
|
> - Tableau opérateur : `admin.gk2.secubox.in/toolbox/#social`.
|
||||||
|
> - Brief poster : [docs/marketing/POSTER-you-have-been-tracked.md](docs/marketing/POSTER-you-have-been-tracked.md)
|
||||||
|
> - Plan + design lock : [#502](https://github.com/CyberMind-FR/secubox-deb/issues/502)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗡️ kbin — le premier outil du couteau suisse cyber
|
||||||
|
|
||||||
|
**kbin** (`kbin.gk2.secubox.in`) est le portail public de la **ToolBoX** SecuBox — la
|
||||||
|
*cabine numérique* et **première lame du couteau suisse cyber modulaire** de
|
||||||
|
[cybermind.fr](https://cybermind.fr). On s'y branche, on surfe normalement, et la lame
|
||||||
|
inspecte et protège le trafic de façon transparente :
|
||||||
|
|
||||||
|
| 🗡️ | Lame |
|
||||||
|
|----|------|
|
||||||
|
| ⚡ | **Performance transparente** — on ne déchiffre que ce qu'on modifie (SNI-splice sélectif) |
|
||||||
|
| 🔒 | **Full encrypted** — inspection MITM complète, forge de cert par hôte, fingerprint Chrome uTLS |
|
||||||
|
| ☠️ | **Injection de poison & smog** — le trafic ad-tech ressort empoisonné, pas seulement bloqué |
|
||||||
|
| 🚫 | **Bandeau anti-adware** — transparence injectée, immune au CSP, SPA-aware |
|
||||||
|
| 🛡️ | **Safe browsing** — Vortex DNS + blacklist nft + détection anti-bot |
|
||||||
|
|
||||||
|
> **Prochaine lame — 🧅 mode Tor quick-switch ([#683](https://github.com/CyberMind-FR/secubox-deb/issues/683)).**
|
||||||
|
> Un tap → le surf ressort par le réseau Tor (egress sortant, pseudo-network) : l'inspection
|
||||||
|
> reste intacte, seule l'**IP de sortie** devient anonyme. Fail-closed, opt-in, sans fuite DNS.
|
||||||
|
|
||||||
|
- Use-case : [docs/wiki/Kbin-Toolbox.md](docs/wiki/Kbin-Toolbox.md)
|
||||||
|
- FAQ : [docs/FAQ-KBIN-TOR.md](docs/FAQ-KBIN-TOR.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## License — CyberMind Source-Disclosed (CMSD-1.0)
|
## License — CyberMind Source-Disclosed (CMSD-1.0)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ network:
|
||||||
bridges:
|
bridges:
|
||||||
br-lan:
|
br-lan:
|
||||||
interfaces: [lan0, lan1, lan2, lan3]
|
interfaces: [lan0, lan1, lan2, lan3]
|
||||||
addresses: [192.168.1.1/24]
|
addresses: [192.168.10.1/24]
|
||||||
dhcp4: false
|
dhcp4: false
|
||||||
parameters:
|
parameters:
|
||||||
stp: false
|
stp: false
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ network:
|
||||||
bridges:
|
bridges:
|
||||||
br-lan:
|
br-lan:
|
||||||
interfaces: [lan0, lan1]
|
interfaces: [lan0, lan1]
|
||||||
addresses: [192.168.1.1/24]
|
addresses: [192.168.10.1/24]
|
||||||
dhcp4: false
|
dhcp4: false
|
||||||
parameters:
|
parameters:
|
||||||
stp: false
|
stp: false
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,21 @@ network:
|
||||||
renderer: networkd
|
renderer: networkd
|
||||||
|
|
||||||
ethernets:
|
ethernets:
|
||||||
# WAN — connecté à l'opérateur
|
# WAN candidate (SFP+, eth0) — connecté à l'opérateur via fibre/module SFP.
|
||||||
eth0:
|
eth0:
|
||||||
dhcp4: true
|
dhcp4: true
|
||||||
dhcp6: false
|
dhcp6: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
# LAN — ports GbE (DSA ou directs selon la config switch)
|
# LAN — port GbE switch (DSA 88E6341)
|
||||||
eth1:
|
eth1:
|
||||||
optional: true
|
optional: true
|
||||||
|
# WAN candidate (RJ45 cuivre, eth2 = mvpp2-2). Sur MOCHAbin le seul RJ45
|
||||||
|
# direct ; sert d'uplink quand l'opérateur arrive en cuivre. Le port WAN
|
||||||
|
# câblé (eth0 SFP+ OU eth2 cuivre) obtient le bail DHCP ; l'autre reste idle.
|
||||||
eth2:
|
eth2:
|
||||||
|
dhcp4: true
|
||||||
|
dhcp6: false
|
||||||
optional: true
|
optional: true
|
||||||
eth3:
|
eth3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
@ -31,8 +36,8 @@ network:
|
||||||
bridges:
|
bridges:
|
||||||
# Bridge LAN
|
# Bridge LAN
|
||||||
br-lan:
|
br-lan:
|
||||||
interfaces: [eth1, eth2, eth3, eth4]
|
interfaces: [eth1, eth3, eth4]
|
||||||
addresses: [192.168.1.1/24]
|
addresses: [192.168.10.1/24]
|
||||||
dhcp4: false
|
dhcp4: false
|
||||||
parameters:
|
parameters:
|
||||||
stp: false
|
stp: false
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ network:
|
||||||
|
|
||||||
# LAN — Interface 2 QEMU (si configurée)
|
# LAN — Interface 2 QEMU (si configurée)
|
||||||
enp0s2:
|
enp0s2:
|
||||||
addresses: [192.168.100.1/24]
|
addresses: [192.168.10.1/24]
|
||||||
dhcp4: false
|
dhcp4: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ network:
|
||||||
|
|
||||||
# LAN — Interface 2 VirtualBox (Internal Network ou Host-Only)
|
# LAN — Interface 2 VirtualBox (Internal Network ou Host-Only)
|
||||||
enp0s8:
|
enp0s8:
|
||||||
addresses: [192.168.100.1/24]
|
addresses: [192.168.10.1/24]
|
||||||
dhcp4: false
|
dhcp4: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ network:
|
||||||
br-lan:
|
br-lan:
|
||||||
interfaces: []
|
interfaces: []
|
||||||
addresses:
|
addresses:
|
||||||
- 192.168.1.1/24
|
- 192.168.10.1/24
|
||||||
dhcp4: false
|
dhcp4: false
|
||||||
optional: true
|
optional: true
|
||||||
parameters:
|
parameters:
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ network:
|
||||||
bridges:
|
bridges:
|
||||||
br-lan:
|
br-lan:
|
||||||
interfaces: [enp0s8]
|
interfaces: [enp0s8]
|
||||||
addresses: [192.168.1.1/24]
|
addresses: [192.168.10.1/24]
|
||||||
dhcp4: false
|
dhcp4: false
|
||||||
parameters:
|
parameters:
|
||||||
stp: false
|
stp: false
|
||||||
|
|
|
||||||
11
clients/android-toolbox/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Android / Gradle build artifacts
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
app/build/
|
||||||
|
local.properties
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
*.keystore
|
||||||
|
*.jks
|
||||||
|
.idea/
|
||||||
|
captures/
|
||||||
74
clients/android-toolbox/README.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||||
|
# SecuBox Android ToolBox client (#531)
|
||||||
|
|
||||||
|
One-tap **R3 onboarding** for the VILLAGE3B cabine : install the CA,
|
||||||
|
import the WireGuard profile, verify the tunnel, then open the live
|
||||||
|
*cartographie sociale*. Replaces the manual Android tutorial.
|
||||||
|
|
||||||
|
## Flow (manual path)
|
||||||
|
1. **Discover** — scan the kbin QR or type the booth host (`kbin.gk2.secubox.in`).
|
||||||
|
2. **Install CA** — downloads `/wg/ca.crt`, launches the Android cert-install intent (`KeyChain.createInstallIntent`).
|
||||||
|
3. **Import profile** — downloads `/wg/profile/new`, hands the `.conf` to the WireGuard app via `FileProvider` + `ACTION_VIEW`.
|
||||||
|
4. **Verify** — polls `/wg/r3-check` → "Tunnel R3 actif ✓".
|
||||||
|
5. **Live metrics** — opens `/social/me` (cartographie sociale).
|
||||||
|
|
||||||
|
## Root path — real zero-tap, fully automated (#538, #551, #558)
|
||||||
|
On a **rooted** device the app onboards with **zero taps**, two ways:
|
||||||
|
|
||||||
|
- **On launch** — auto-detects root and runs the silent sequence immediately
|
||||||
|
every launch (no gate), retrying reachability while WiFi/tunnel settle.
|
||||||
|
- **On boot** — a `BOOT_COMPLETED` receiver starts a short foreground service
|
||||||
|
(`OnboardService`) that runs the same silent sequence **without opening the
|
||||||
|
app**, then stops. After one reboot the device self-onboards.
|
||||||
|
|
||||||
|
The **⚡ Installation automatique (root)** button remains as a manual
|
||||||
|
re-trigger. Two interactions are **mandated by Android and unavoidable** for
|
||||||
|
any app: the sideload install confirm ("install unknown apps") and the
|
||||||
|
first-time superuser (Magisk/su) grant prompt. Everything after those is
|
||||||
|
zero-tap. Steps:
|
||||||
|
|
||||||
|
1. **System CA install** — downloads `/wg/ca.pem`, computes the OpenSSL
|
||||||
|
`subject_hash_old` in pure Kotlin, and bind-mounts a populated copy of
|
||||||
|
the trust store over `/system/etc/security/cacerts` (+ the conscrypt
|
||||||
|
APEX path on Android 14), restoring the SELinux context
|
||||||
|
(`u:object_r:system_security_cacerts_file:s0`). **Every** app trusts the
|
||||||
|
cabine CA — not just user-CA opt-in apps. Reversible via `umount`.
|
||||||
|
2. **Native WireGuard** — if the kernel has the WireGuard module + `wg`/`ip`,
|
||||||
|
brings the tunnel up natively (`ip link add … type wireguard` + `wg set`),
|
||||||
|
no WireGuard app required.
|
||||||
|
3. **Auto R3 verify** — polls `/wg/r3-check`.
|
||||||
|
|
||||||
|
**Fallback** — if the kernel lacks WireGuard, the root path installs the
|
||||||
|
system CA then hands off to the manual WireGuard-app flow (steps 3–5 above).
|
||||||
|
|
||||||
|
All root actions are **gated behind the explicit tap** — nothing runs as
|
||||||
|
root without the operator choosing root mode on their own device.
|
||||||
|
See `RootShell.kt` (su wrapper) and `RootOnboard.kt` (silent sequence).
|
||||||
|
|
||||||
|
## Build
|
||||||
|
No Gradle wrapper jar is committed (text-only scaffold). CI builds it:
|
||||||
|
- **GitHub Actions** `build-android-apk.yml` → debug APK artifact.
|
||||||
|
Locally (with Android SDK + Gradle 8.9 + JDK 17):
|
||||||
|
```bash
|
||||||
|
cd clients/android-toolbox
|
||||||
|
gradle :app:assembleDebug # app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Constraints (MVP)
|
||||||
|
- Android 11+ restricts **user CA trust** ; the *manual* path launches the
|
||||||
|
install intent + guides the confirm step. Browsers on the device need the
|
||||||
|
CA trusted for the mitm R3 break — this is the known Android limitation on
|
||||||
|
non-rooted devices. **Rooted devices bypass it entirely** via the system
|
||||||
|
CA install (see Root path above).
|
||||||
|
- The *manual* path imports the WireGuard profile via the **official
|
||||||
|
WireGuard app** (no embedded tunnel) — most reliable, no extra native
|
||||||
|
deps. The *root* path brings the tunnel up natively with the kernel module.
|
||||||
|
- Debug APK is self-signed (sideload). Release signing (published
|
||||||
|
fingerprint, served from the toolbox) is a follow-up needing a keystore
|
||||||
|
secret in CI.
|
||||||
|
|
||||||
|
## Tech
|
||||||
|
Kotlin + Jetpack Compose, minSdk 26 / targetSdk 34. API client is plain
|
||||||
|
`HttpURLConnection` (no Retrofit/OkHttp) to keep deps + CI minimal.
|
||||||
|
|
||||||
|
Package `in.secubox.toolbox`. License `LicenseRef-CMSD-1.0`.
|
||||||
49
clients/android-toolbox/app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "in.secubox.toolbox"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "in.secubox.toolbox"
|
||||||
|
minSdk = 26
|
||||||
|
targetSdk = 34
|
||||||
|
versionCode = 4
|
||||||
|
versionName = "0.4.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
// Signed in CI with a published-fingerprint key (sideload APK,
|
||||||
|
// no Play Store). Debug builds are self-signed by the SDK.
|
||||||
|
signingConfig = signingConfigs.findByName("release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions { jvmTarget = "17" }
|
||||||
|
buildFeatures { compose = true }
|
||||||
|
composeOptions { kotlinCompilerExtensionVersion = "1.5.14" }
|
||||||
|
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
val composeBom = platform("androidx.compose:compose-bom:2024.06.00")
|
||||||
|
implementation(composeBom)
|
||||||
|
implementation("androidx.core:core-ktx:1.13.1")
|
||||||
|
implementation("androidx.activity:activity-compose:1.9.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
|
||||||
|
implementation("androidx.compose.ui:ui")
|
||||||
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
|
implementation("androidx.compose.material3:material3")
|
||||||
|
implementation("androidx.compose.material:material-icons-extended")
|
||||||
|
// No Retrofit/OkHttp — the API client uses HttpURLConnection to keep
|
||||||
|
// the dependency graph (and CI) minimal.
|
||||||
|
}
|
||||||
66
clients/android-toolbox/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<!-- #558 full-auto: run the silent onboarding on device boot, no app open. -->
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<!-- Query the WireGuard app so we can hand it the generated profile. -->
|
||||||
|
<queries>
|
||||||
|
<package android:name="com.wireguard.android" />
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:theme="@style/Theme.SecuBoxToolBox"
|
||||||
|
android:supportsRtl="true">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<!-- #558 : boot-completed → start the onboarding foreground service
|
||||||
|
so a rooted device self-onboards with zero taps after a reboot. -->
|
||||||
|
<receiver
|
||||||
|
android:name=".BootReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:enabled="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
<service
|
||||||
|
android:name=".OnboardService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="specialUse">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="Silent R3 onboarding on a rooted, operator-owned cabine device" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<!-- FileProvider to share the downloaded CA + WG .conf with the
|
||||||
|
system cert installer / the WireGuard app. -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
//
|
||||||
|
// #558 — boot-completed → kick the onboarding foreground service so a
|
||||||
|
// rooted, operator-owned cabine device self-onboards with zero taps after
|
||||||
|
// a reboot (no need to open the app).
|
||||||
|
package `in`.secubox.toolbox
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
val a = intent?.action ?: return
|
||||||
|
if (a == Intent.ACTION_BOOT_COMPLETED || a == Intent.ACTION_LOCKED_BOOT_COMPLETED) {
|
||||||
|
ContextCompat.startForegroundService(
|
||||||
|
context, Intent(context, OnboardService::class.java),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,323 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
//
|
||||||
|
// SecuBox-Deb :: Android ToolBox client (#531)
|
||||||
|
// One-tap R3 onboarding : discover -> install CA -> import WG profile ->
|
||||||
|
// verify tunnel -> live cartographie sociale. Replaces the manual
|
||||||
|
// multi-step Android tutorial.
|
||||||
|
package `in`.secubox.toolbox
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.security.KeyChain
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
private val Cosmos = Color(0xFF0A0A0F)
|
||||||
|
private val Gold = Color(0xFFC9A84C)
|
||||||
|
private val Cyan = Color(0xFF00D4FF)
|
||||||
|
private val Matrix = Color(0xFF00FF41)
|
||||||
|
private val Cinnabar = Color(0xFFE63946)
|
||||||
|
private val TextPrimary = Color(0xFFE8E6D9)
|
||||||
|
|
||||||
|
enum class Step { Discover, RootAuto, InstallCa, ImportProfile, Verify, Done }
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent { OnboardApp() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun OnboardApp() {
|
||||||
|
val ctx = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var host by remember { mutableStateOf("kbin.gk2.secubox.in") }
|
||||||
|
var step by remember { mutableStateOf(Step.Discover) }
|
||||||
|
var status by remember { mutableStateOf("") }
|
||||||
|
var busy by remember { mutableStateOf(false) }
|
||||||
|
var onTunnel by remember { mutableStateOf(false) }
|
||||||
|
var peerIp by remember { mutableStateOf<String?>(null) }
|
||||||
|
val api = remember(host) { ToolboxApi(host) }
|
||||||
|
var rootAvail by remember { mutableStateOf(false) }
|
||||||
|
val rootLog = remember { mutableStateListOf<String>() }
|
||||||
|
val prefs = remember {
|
||||||
|
ctx.getSharedPreferences("secubox-toolbox", android.content.Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
var autoTried by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// The whole root-mode silent run, reused by the ⚡ button AND the
|
||||||
|
// zero-tap auto-launch (#551/#558). NO onboarded gate — it auto-runs
|
||||||
|
// every launch (idempotent: re-asserts CA + WG). Reachability is
|
||||||
|
// RETRIED so a WiFi/tunnel race at launch doesn't kill the auto-run.
|
||||||
|
val runRootAuto: () -> Unit = {
|
||||||
|
busy = true; status = ""; rootLog.clear()
|
||||||
|
scope.launch {
|
||||||
|
// poll reachability up to ~9 s (network may still be settling)
|
||||||
|
var ok = false
|
||||||
|
for (attempt in 1..6) {
|
||||||
|
ok = withContext(Dispatchers.IO) { api.reachable() }
|
||||||
|
if (ok) break
|
||||||
|
status = "Recherche de la borne… ($attempt)"
|
||||||
|
kotlinx.coroutines.delay(1500)
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
busy = false; status = "Borne injoignable — vérifie le réseau."
|
||||||
|
} else {
|
||||||
|
step = Step.RootAuto
|
||||||
|
val onb = RootOnboard(api, ctx.cacheDir, ctx.filesDir)
|
||||||
|
val out = withContext(Dispatchers.IO) {
|
||||||
|
onb.runSilent { line -> scope.launch(Dispatchers.Main) { rootLog.add(line) } }
|
||||||
|
}
|
||||||
|
busy = false
|
||||||
|
onTunnel = out.verified
|
||||||
|
// #683 — surface kbin Tor egress status (anonymised exit) if on.
|
||||||
|
rootLog.add(withContext(Dispatchers.IO) {
|
||||||
|
val t = api.torStatus()
|
||||||
|
when {
|
||||||
|
t == null -> "• Statut Tor : indisponible"
|
||||||
|
!t.optBoolean("tor_mode", false) -> "• Mode Tor : inactif"
|
||||||
|
t.optBoolean("running", false) ->
|
||||||
|
"🧅 Mode Tor ACTIF — sortie anonymisée${t.optString("exit_ip", "").let { if (it.isNotBlank() && it != "null") " ($it)" else "" }}"
|
||||||
|
else -> "🧅 Mode Tor activé — tunnel Tor en démarrage…"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
when {
|
||||||
|
out.verified -> step = Step.Done
|
||||||
|
out.wgViaApp -> { step = Step.ImportProfile
|
||||||
|
status = "CA installé en root ✓ — termine le tunnel via l'app WireGuard." }
|
||||||
|
else -> { step = Step.Verify
|
||||||
|
status = "Active le tunnel puis vérifie." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect root once, off the main thread.
|
||||||
|
LaunchedEffect(Unit) { rootAvail = withContext(Dispatchers.IO) { RootShell.available() } }
|
||||||
|
// Zero-tap (#558): on a rooted device, auto-run the silent onboarding
|
||||||
|
// on every launch — no gate. (Boot-time auto-run is handled by
|
||||||
|
// BootReceiver + OnboardService so it runs without opening the app.)
|
||||||
|
LaunchedEffect(rootAvail) {
|
||||||
|
if (rootAvail && !autoTried && step == Step.Discover) {
|
||||||
|
autoTried = true
|
||||||
|
runRootAuto()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme(
|
||||||
|
primary = Gold, secondary = Cyan, background = Cosmos, surface = Cosmos,
|
||||||
|
onBackground = TextPrimary, onSurface = TextPrimary,
|
||||||
|
)) {
|
||||||
|
Surface(Modifier.fillMaxSize(), color = Cosmos) {
|
||||||
|
Column(
|
||||||
|
Modifier.fillMaxSize().padding(20.dp).verticalScroll(rememberScrollState()),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text("📡 VILLAGE3B", color = Gold, fontSize = 26.sp, fontWeight = FontWeight.Bold)
|
||||||
|
Text("ToolBoX — installation R3", color = TextPrimary, fontSize = 14.sp)
|
||||||
|
Spacer(Modifier.height(20.dp))
|
||||||
|
|
||||||
|
Stepper(step)
|
||||||
|
Spacer(Modifier.height(20.dp))
|
||||||
|
|
||||||
|
when (step) {
|
||||||
|
Step.Discover -> {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = host, onValueChange = { host = it },
|
||||||
|
label = { Text("Borne (kbin…)") }, singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text("Scanne le QR de la cabine ou saisis l'adresse, puis Suivant.",
|
||||||
|
color = TextPrimary, fontSize = 12.sp)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
BigButton("Suivant", busy) {
|
||||||
|
busy = true; status = "Vérification de la borne…"
|
||||||
|
scope.launch {
|
||||||
|
val ok = withContext(Dispatchers.IO) { api.reachable() }
|
||||||
|
busy = false
|
||||||
|
if (ok) { step = Step.InstallCa; status = "" }
|
||||||
|
else status = "Borne injoignable — vérifie l'adresse / le réseau."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rootAvail) {
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
Text("🔓 Root détecté — l'installation se lance automatiquement. " +
|
||||||
|
"Tu peux aussi la relancer ici.",
|
||||||
|
color = Matrix, fontSize = 12.sp)
|
||||||
|
Spacer(Modifier.height(6.dp))
|
||||||
|
OutlinedButton(onClick = runRootAuto, modifier = Modifier.fillMaxWidth(),
|
||||||
|
border = BorderStroke(1.dp, Matrix),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(contentColor = Matrix)) {
|
||||||
|
Text("⚡ Installation automatique (root)", fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Step.RootAuto -> {
|
||||||
|
StepBody("Installation automatique (root)",
|
||||||
|
"CA système + tunnel WireGuard, sans intervention.")
|
||||||
|
Surface(color = Color(0xFF0E0E15), shape = MaterialTheme.shapes.small,
|
||||||
|
modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(Modifier.padding(12.dp)) {
|
||||||
|
rootLog.forEach { line ->
|
||||||
|
Text(line, color = if (line.startsWith("✗")) Cinnabar
|
||||||
|
else if (line.startsWith("✓")) Matrix else TextPrimary,
|
||||||
|
fontSize = 12.sp, fontFamily = FontFamily.Monospace)
|
||||||
|
}
|
||||||
|
if (busy) {
|
||||||
|
Spacer(Modifier.height(6.dp))
|
||||||
|
CircularProgressIndicator(Modifier.size(18.dp), color = Gold, strokeWidth = 2.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Step.InstallCa -> {
|
||||||
|
StepBody("1 · Installer le certificat (CA R3)",
|
||||||
|
"Le certificat permet l'analyse TLS de la cabine. " +
|
||||||
|
"Android te demandera de confirmer (Paramètres → Sécurité → " +
|
||||||
|
"Certificat utilisateur).")
|
||||||
|
BigButton("Installer le certificat", busy) {
|
||||||
|
busy = true
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val ca = withContext(Dispatchers.IO) { api.downloadCa(ctx.cacheDir) }
|
||||||
|
val der = ca.readBytes()
|
||||||
|
val intent = KeyChain.createInstallIntent().apply {
|
||||||
|
putExtra(KeyChain.EXTRA_CERTIFICATE, der)
|
||||||
|
putExtra(KeyChain.EXTRA_NAME, "VILLAGE3B ToolBoX CA")
|
||||||
|
}
|
||||||
|
ctx.startActivity(intent)
|
||||||
|
status = "Confirme l'installation dans Android, puis Suivant."
|
||||||
|
} catch (e: Exception) {
|
||||||
|
status = "Échec téléchargement CA : ${e.message}"
|
||||||
|
} finally { busy = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
TextButton(onClick = {
|
||||||
|
ctx.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||||
|
}) { Text("Ouvrir Paramètres sécurité", color = Cyan) }
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
BigButton("Suivant", false) { step = Step.ImportProfile; status = "" }
|
||||||
|
}
|
||||||
|
Step.ImportProfile -> {
|
||||||
|
StepBody("2 · Importer le profil WireGuard",
|
||||||
|
"On génère un profil dédié et on l'ouvre dans l'app WireGuard. " +
|
||||||
|
"Active le tunnel dans WireGuard, puis reviens ici.")
|
||||||
|
BigButton("Importer dans WireGuard", busy) {
|
||||||
|
busy = true
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val conf = withContext(Dispatchers.IO) { api.downloadProfile(ctx.cacheDir) }
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
ctx, "${ctx.packageName}.fileprovider", conf)
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, "application/octet-stream")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
setPackage("com.wireguard.android")
|
||||||
|
}
|
||||||
|
try { ctx.startActivity(intent) }
|
||||||
|
catch (_: Exception) {
|
||||||
|
// WireGuard not installed -> open Play / generic chooser.
|
||||||
|
ctx.startActivity(Intent(Intent.ACTION_VIEW,
|
||||||
|
Uri.parse("https://play.google.com/store/apps/details?id=com.wireguard.android")))
|
||||||
|
status = "Installe l'app WireGuard puis réessaie."
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
status = "Échec profil : ${e.message}"
|
||||||
|
} finally { busy = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
BigButton("Suivant", false) { step = Step.Verify; status = "" }
|
||||||
|
}
|
||||||
|
Step.Verify -> {
|
||||||
|
StepBody("3 · Vérifier le tunnel R3",
|
||||||
|
"Active le tunnel dans WireGuard, puis vérifie.")
|
||||||
|
BigButton("Vérifier", busy) {
|
||||||
|
busy = true; status = "Vérification…"
|
||||||
|
scope.launch {
|
||||||
|
val (t, ip) = withContext(Dispatchers.IO) { api.r3Check() }
|
||||||
|
busy = false; onTunnel = t; peerIp = ip
|
||||||
|
if (t) { step = Step.Done; status = "" }
|
||||||
|
else status = "Pas encore sur le tunnel — active WireGuard puis réessaie."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Step.Done -> {
|
||||||
|
Icon(Icons.Filled.CheckCircle, null, tint = Matrix, modifier = Modifier.size(56.dp))
|
||||||
|
Text("Tunnel R3 actif ✓", color = Matrix, fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||||
|
peerIp?.let { Text("pair : $it", color = TextPrimary, fontSize = 12.sp) }
|
||||||
|
Spacer(Modifier.height(20.dp))
|
||||||
|
BigButton("🕸️ Voir ma cartographie sociale", false) {
|
||||||
|
ctx.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(api.socialMeUrl)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.isNotBlank()) {
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Text(status, color = if (status.contains("Échec") || status.contains("injoignable")) Cinnabar else Cyan,
|
||||||
|
fontSize = 13.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Stepper(cur: Step) {
|
||||||
|
val steps = listOf(Step.Discover, Step.InstallCa, Step.ImportProfile, Step.Verify, Step.Done)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
steps.forEach { s ->
|
||||||
|
val done = s.ordinal < cur.ordinal
|
||||||
|
val active = s == cur
|
||||||
|
Box(Modifier.size(if (active) 14.dp else 10.dp)) {
|
||||||
|
Surface(shape = MaterialTheme.shapes.small,
|
||||||
|
color = when { done -> Matrix; active -> Gold; else -> Color(0xFF333333) },
|
||||||
|
modifier = Modifier.fillMaxSize()) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StepBody(title: String, body: String) {
|
||||||
|
Text(title, color = Gold, fontSize = 16.sp, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(body, color = TextPrimary, fontSize = 13.sp)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BigButton(label: String, busy: Boolean, onClick: () -> Unit) {
|
||||||
|
Button(onClick = onClick, enabled = !busy, modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Gold, contentColor = Cosmos)) {
|
||||||
|
if (busy) CircularProgressIndicator(Modifier.size(18.dp), color = Cosmos, strokeWidth = 2.dp)
|
||||||
|
else Text(label, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
//
|
||||||
|
// #558 — full-auto onboarding service. Started on boot (BootReceiver). On a
|
||||||
|
// rooted device it runs the silent R3 onboarding (system CA + native WG +
|
||||||
|
// verify) with zero taps, retrying reachability while the network settles,
|
||||||
|
// then stops itself. Non-root / unreachable → it just stops (the launcher
|
||||||
|
// activity remains the manual path).
|
||||||
|
package `in`.secubox.toolbox
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class OnboardService : Service() {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
private val CHAN = "sbx-onboard"
|
||||||
|
private val NID = 4201
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
startForeground(NID, buildNotification())
|
||||||
|
scope.launch {
|
||||||
|
try { runOnce() } finally { stopSelf() }
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun runOnce() {
|
||||||
|
// root is the precondition for the silent path; bail quietly otherwise.
|
||||||
|
if (!RootShell.available()) return
|
||||||
|
val host = getSharedPreferences("secubox-toolbox", Context.MODE_PRIVATE)
|
||||||
|
.getString("host", null) ?: "kbin.gk2.secubox.in"
|
||||||
|
val api = ToolboxApi(host)
|
||||||
|
// network may still be coming up after boot — retry ~30 s.
|
||||||
|
var ok = false
|
||||||
|
for (i in 1..15) {
|
||||||
|
ok = api.reachable()
|
||||||
|
if (ok) break
|
||||||
|
kotlinx.coroutines.delay(2000)
|
||||||
|
}
|
||||||
|
if (!ok) return
|
||||||
|
RootOnboard(api, cacheDir, filesDir).runSilent { /* headless: no UI log */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNotification(): Notification {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val nm = getSystemService(NotificationManager::class.java)
|
||||||
|
nm?.createNotificationChannel(
|
||||||
|
NotificationChannel(CHAN, "SecuBox onboarding",
|
||||||
|
NotificationManager.IMPORTANCE_LOW),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val b = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
Notification.Builder(this, CHAN) else @Suppress("DEPRECATION") Notification.Builder(this)
|
||||||
|
return b.setContentTitle("VILLAGE3B")
|
||||||
|
.setContentText("Activation R3 automatique…")
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
scope.coroutineContext[kotlinx.coroutines.Job]?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
//
|
||||||
|
// Root-mode fully-automated silent R3 onboarding (#538).
|
||||||
|
// All actions are gated behind an explicit "root auto" tap in the UI.
|
||||||
|
package `in`.secubox.toolbox
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
|
||||||
|
class RootOnboard(
|
||||||
|
private val api: ToolboxApi,
|
||||||
|
private val cacheDir: File,
|
||||||
|
// #683: app-internal storage for the STABLE WG identity (survives reboot).
|
||||||
|
// Defaults to cacheDir so older call sites still compile, but real callers
|
||||||
|
// pass filesDir so the identity persists instead of churning each boot.
|
||||||
|
private val filesDir: File = cacheDir,
|
||||||
|
) {
|
||||||
|
|
||||||
|
/** A line appended to the on-screen log during the silent run. */
|
||||||
|
fun interface Logger { fun log(line: String) }
|
||||||
|
|
||||||
|
data class Outcome(val caInstalled: Boolean, val wgUp: Boolean, val verified: Boolean,
|
||||||
|
val wgViaApp: Boolean)
|
||||||
|
|
||||||
|
// ── system CA install (silent, root) ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenSSL `subject_hash_old` (pre-1.0 hash) computed WITHOUT openssl :
|
||||||
|
* MD5 of the DER-encoded subject name, first 4 bytes as a uint32
|
||||||
|
* little-endian, formatted "%08x". This is the filename the Android
|
||||||
|
* system cacerts store uses (<hash>.0).
|
||||||
|
*/
|
||||||
|
fun subjectHashOld(pem: ByteArray): String {
|
||||||
|
val cf = CertificateFactory.getInstance("X.509")
|
||||||
|
val cert = cf.generateCertificate(pem.inputStream()) as java.security.cert.X509Certificate
|
||||||
|
val subjectDer = cert.subjectX500Principal.encoded
|
||||||
|
val md5 = MessageDigest.getInstance("MD5").digest(subjectDer)
|
||||||
|
val h = (md5[0].toLong() and 0xff) or
|
||||||
|
((md5[1].toLong() and 0xff) shl 8) or
|
||||||
|
((md5[2].toLong() and 0xff) shl 16) or
|
||||||
|
((md5[3].toLong() and 0xff) shl 24)
|
||||||
|
return String.format("%08x", h)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install the CA into the SYSTEM trust store so EVERY app (not just
|
||||||
|
* those opting into user CAs) trusts it. Uses the bind-mount-over-
|
||||||
|
* cacerts technique that works on Android 10–14 (incl. the conscrypt
|
||||||
|
* APEX). Non-persistent across reboot — fine for a temporary cabine
|
||||||
|
* diagnostic; the app can also unmount to revert.
|
||||||
|
*/
|
||||||
|
fun installCaSystem(log: Logger): Boolean {
|
||||||
|
log.log("• Téléchargement du CA…")
|
||||||
|
val pem = api.download("/wg/ca.pem", "village3b-ca.pem", cacheDir).readBytes()
|
||||||
|
val hash = subjectHashOld(pem)
|
||||||
|
log.log("• CA hash : $hash.0")
|
||||||
|
val local = File(cacheDir, "$hash.0").apply { writeBytes(pem) }
|
||||||
|
|
||||||
|
// Push the cert to a root-readable scratch path, then bind-mount a
|
||||||
|
// populated copy of the system store over the live cacerts dir.
|
||||||
|
val pushed = "/data/local/tmp/sbx-$hash.0"
|
||||||
|
val push = RootShell.install(local, pushed, "644")
|
||||||
|
if (!push.ok) { log.log("✗ push échoué : ${push.err.trim()}"); return false }
|
||||||
|
|
||||||
|
val r = RootShell.runScript(
|
||||||
|
"set -e",
|
||||||
|
"CERT_DIR=/system/etc/security/cacerts",
|
||||||
|
"TMP=/data/local/tmp/sbx-cacerts",
|
||||||
|
"rm -rf \$TMP; mkdir -p \$TMP",
|
||||||
|
// seed with the existing system + APEX certs so nothing is lost
|
||||||
|
"cp -f \$CERT_DIR/* \$TMP/ 2>/dev/null || true",
|
||||||
|
"cp -f /apex/com.android.conscrypt/cacerts/* \$TMP/ 2>/dev/null || true",
|
||||||
|
"cp -f $pushed \$TMP/$hash.0",
|
||||||
|
"chmod 644 \$TMP/* 2>/dev/null || true",
|
||||||
|
"chown 0:0 \$TMP/* 2>/dev/null || true",
|
||||||
|
"chcon u:object_r:system_security_cacerts_file:s0 \$TMP/* 2>/dev/null || true",
|
||||||
|
// bind-mount over the live store (and the APEX path on 14)
|
||||||
|
"mount -o bind \$TMP \$CERT_DIR",
|
||||||
|
"[ -d /apex/com.android.conscrypt/cacerts ] && mount -o bind \$TMP /apex/com.android.conscrypt/cacerts 2>/dev/null || true",
|
||||||
|
"echo OK",
|
||||||
|
)
|
||||||
|
if (r.ok && r.out.contains("OK")) {
|
||||||
|
log.log("✓ CA installé dans le magasin système (toutes les apps le font confiance)")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
log.log("✗ install CA système : ${r.err.trim().ifBlank { r.out.trim() }}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCaSystem(log: Logger): Boolean {
|
||||||
|
val r = RootShell.runScript(
|
||||||
|
"umount /system/etc/security/cacerts 2>/dev/null || true",
|
||||||
|
"umount /apex/com.android.conscrypt/cacerts 2>/dev/null || true",
|
||||||
|
"echo OK",
|
||||||
|
)
|
||||||
|
log.log(if (r.ok) "✓ CA système retiré (démonté)" else "✗ démontage : ${r.err.trim()}")
|
||||||
|
return r.ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WireGuard bring-up ──
|
||||||
|
|
||||||
|
/** Parse the wg-quick .conf into the fields we need. */
|
||||||
|
private data class WgConf(val privKey: String, val address: String,
|
||||||
|
val pubKey: String, val endpoint: String, val allowed: String)
|
||||||
|
|
||||||
|
private fun parse(conf: String): WgConf? {
|
||||||
|
var pk = ""; var addr = ""; var pub = ""; var ep = ""; var aip = ""
|
||||||
|
conf.lineSequence().forEach { raw ->
|
||||||
|
val l = raw.trim()
|
||||||
|
when {
|
||||||
|
l.startsWith("PrivateKey", true) -> pk = l.substringAfter("=").trim()
|
||||||
|
l.startsWith("Address", true) -> addr = l.substringAfter("=").trim()
|
||||||
|
l.startsWith("PublicKey", true) -> pub = l.substringAfter("=").trim()
|
||||||
|
l.startsWith("Endpoint", true) -> ep = l.substringAfter("=").trim()
|
||||||
|
l.startsWith("AllowedIPs", true) -> aip = l.substringAfter("=").trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (pk.isNotBlank() && pub.isNotBlank() && ep.isNotBlank()) WgConf(pk, addr, pub, ep, aip) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bring the tunnel up natively with root IF the kernel has WireGuard
|
||||||
|
* + `wg`/`ip`. Returns true on success ; false means the caller
|
||||||
|
* should fall back to the WireGuard-app handoff.
|
||||||
|
*/
|
||||||
|
fun setupWireguardRoot(log: Logger): Boolean {
|
||||||
|
if (!RootShell.hasKernelWireguard()) {
|
||||||
|
log.log("• Noyau sans module WireGuard — bascule sur l'app WireGuard")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
log.log("• Profil WireGuard (identité stable)…")
|
||||||
|
// #683: reuse the persisted keypair so the device keeps ONE identity
|
||||||
|
// across reboots (no more stats reset to a fresh empty hash each boot).
|
||||||
|
val conf = api.persistentProfile(filesDir).readText()
|
||||||
|
val wg = parse(conf) ?: run { log.log("✗ profil illisible"); return false }
|
||||||
|
val iface = "wg-village3b"
|
||||||
|
val r = RootShell.runScript(
|
||||||
|
"set -e",
|
||||||
|
"ip link del $iface 2>/dev/null || true",
|
||||||
|
"ip link add $iface type wireguard",
|
||||||
|
"echo '${wg.privKey}' > /data/local/tmp/sbx-wg.key && chmod 600 /data/local/tmp/sbx-wg.key",
|
||||||
|
"wg set $iface private-key /data/local/tmp/sbx-wg.key peer ${wg.pubKey} endpoint ${wg.endpoint} allowed-ips ${wg.allowed.ifBlank { "0.0.0.0/0" }} persistent-keepalive 25",
|
||||||
|
"rm -f /data/local/tmp/sbx-wg.key",
|
||||||
|
if (wg.address.isNotBlank()) "ip addr add ${wg.address} dev $iface 2>/dev/null || true" else ":",
|
||||||
|
"ip link set $iface up",
|
||||||
|
"for n in ${wg.allowed.replace(",", " ")}; do ip route replace \$n dev $iface 2>/dev/null || true; done",
|
||||||
|
"echo OK",
|
||||||
|
)
|
||||||
|
if (r.ok && r.out.contains("OK")) { log.log("✓ Tunnel $iface actif (root, natif)"); return true }
|
||||||
|
log.log("✗ WG natif : ${r.err.trim().ifBlank { r.out.trim() }} — bascule sur l'app")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run the whole silent sequence. Blocking — call off-main. */
|
||||||
|
fun runSilent(log: Logger): Outcome {
|
||||||
|
val ca = installCaSystem(log)
|
||||||
|
val wgRoot = setupWireguardRoot(log)
|
||||||
|
var verified = false
|
||||||
|
if (wgRoot) {
|
||||||
|
log.log("• Vérification R3…")
|
||||||
|
Thread.sleep(1500)
|
||||||
|
val (t, ip) = api.r3Check()
|
||||||
|
verified = t
|
||||||
|
log.log(if (t) "✓ Tunnel R3 confirmé (${ip ?: "?"})" else "• Pas encore confirmé — réessaie la vérification")
|
||||||
|
}
|
||||||
|
return Outcome(caInstalled = ca, wgUp = wgRoot, verified = verified, wgViaApp = !wgRoot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
package `in`.secubox.toolbox
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thin wrapper around `su` for the root-mode silent onboarding (#538).
|
||||||
|
* Every action is gated behind an explicit user tap in the UI — nothing
|
||||||
|
* runs as root without the operator choosing root mode on their own
|
||||||
|
* device.
|
||||||
|
*/
|
||||||
|
object RootShell {
|
||||||
|
|
||||||
|
data class Result(val code: Int, val out: String, val err: String) {
|
||||||
|
val ok get() = code == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True if a `su` binary is on PATH and grants a root shell. */
|
||||||
|
fun available(): Boolean = try {
|
||||||
|
run("id -u").out.trim() == "0"
|
||||||
|
} catch (_: Exception) { false }
|
||||||
|
|
||||||
|
/** Run a single command in a root shell. Blocking — call off-main. */
|
||||||
|
fun run(cmd: String): Result {
|
||||||
|
val p = ProcessBuilder("su", "-c", cmd)
|
||||||
|
.redirectErrorStream(false)
|
||||||
|
.start()
|
||||||
|
val out = p.inputStream.bufferedReader().readText()
|
||||||
|
val err = p.errorStream.bufferedReader().readText()
|
||||||
|
val code = p.waitFor()
|
||||||
|
return Result(code, out, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run several commands in ONE root shell (atomic-ish, keeps remount). */
|
||||||
|
fun runScript(vararg lines: String): Result {
|
||||||
|
val p = ProcessBuilder("su").redirectErrorStream(false).start()
|
||||||
|
p.outputStream.bufferedWriter().use { w ->
|
||||||
|
lines.forEach { w.write(it); w.write("\n") }
|
||||||
|
w.write("exit $?\n")
|
||||||
|
}
|
||||||
|
val out = p.inputStream.bufferedReader().readText()
|
||||||
|
val err = p.errorStream.bufferedReader().readText()
|
||||||
|
val code = p.waitFor()
|
||||||
|
return Result(code, out, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Push a local file to a root-owned path via cat (avoids cp quirks). */
|
||||||
|
fun install(src: File, destPath: String, mode: String = "644"): Result {
|
||||||
|
val b64 = android.util.Base64.encodeToString(src.readBytes(), android.util.Base64.NO_WRAP)
|
||||||
|
return runScript(
|
||||||
|
"echo '$b64' | base64 -d > '$destPath'",
|
||||||
|
"chmod $mode '$destPath'",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kernel has WireGuard + the `wg` tool available to root? */
|
||||||
|
fun hasKernelWireguard(): Boolean = try {
|
||||||
|
val w = run("command -v wg || ls /system/*bin/wg 2>/dev/null")
|
||||||
|
val ip = run("command -v ip")
|
||||||
|
w.out.isNotBlank() && ip.out.isNotBlank()
|
||||||
|
} catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
package `in`.secubox.toolbox
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal HTTP client for the SecuBox ToolBox R3 endpoints. Uses
|
||||||
|
* HttpURLConnection (no Retrofit/OkHttp) to keep the dependency graph
|
||||||
|
* and CI minimal. All calls are blocking — invoke off the main thread.
|
||||||
|
*
|
||||||
|
* Endpoints (served on the kbin vhost, e.g. kbin.gk2.secubox.in) :
|
||||||
|
* GET /wg/ca.crt -> CA root cert (Android DER/PEM)
|
||||||
|
* GET /wg/profile/new -> wg-quick .conf (one fresh peer per call)
|
||||||
|
* GET /wg/r3-check -> {"tunnel": bool, "peer_ip": "10.99.1.x"}
|
||||||
|
* GET /social/me -> per-client cartographie sociale (web view)
|
||||||
|
*/
|
||||||
|
class ToolboxApi(rawHost: String) {
|
||||||
|
|
||||||
|
// Accept "kbin.gk2.secubox.in", "https://kbin…", trailing slashes…
|
||||||
|
val base: String = rawHost.trim()
|
||||||
|
.removePrefix("https://").removePrefix("http://")
|
||||||
|
.trim('/')
|
||||||
|
.let { "https://$it" }
|
||||||
|
|
||||||
|
val socialMeUrl: String get() = "$base/social/me"
|
||||||
|
|
||||||
|
private fun open(path: String): HttpURLConnection =
|
||||||
|
(URL("$base$path").openConnection() as HttpURLConnection).apply {
|
||||||
|
connectTimeout = 8000
|
||||||
|
readTimeout = 12000
|
||||||
|
setRequestProperty("User-Agent", "secubox-toolbox-android/0.1")
|
||||||
|
instanceFollowRedirects = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Download a file (CA or WG profile) into the app cache, return it. */
|
||||||
|
fun download(path: String, outName: String, cacheDir: File): File {
|
||||||
|
val c = open(path)
|
||||||
|
try {
|
||||||
|
if (c.responseCode !in 200..299)
|
||||||
|
throw RuntimeException("HTTP ${c.responseCode} for $path")
|
||||||
|
val out = File(cacheDir, outName)
|
||||||
|
c.inputStream.use { input -> out.outputStream().use { input.copyTo(it) } }
|
||||||
|
return out
|
||||||
|
} finally { c.disconnect() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadCa(cacheDir: File): File = download("/wg/ca.crt", "village3b-ca.crt", cacheDir)
|
||||||
|
fun downloadProfile(cacheDir: File): File = download("/wg/profile/new", "village3b-toolbox.conf", cacheDir)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The device's STABLE WireGuard identity (#683 lost-referrer fix).
|
||||||
|
*
|
||||||
|
* `/wg/profile/new` mints a FRESH keypair on every call. The onboarding
|
||||||
|
* runs on every boot, so calling it each time gave the device a NEW pubkey
|
||||||
|
* → new sha256(pubkey) identity hash → its stats/social history reset to an
|
||||||
|
* empty bucket on every reboot/reconnect. Here we fetch a peer ONCE and
|
||||||
|
* persist the .conf in app-internal `filesDir` (survives reboots, unlike the
|
||||||
|
* evictable cacheDir). Every later call reuses the SAME keypair → SAME
|
||||||
|
* identity → the device keeps one continuous history.
|
||||||
|
*
|
||||||
|
* Survives reboot/reconnect/app-restart. (Reinstall still wipes filesDir;
|
||||||
|
* cross-reinstall persistence would need allowBackup — kept off for CSPN.)
|
||||||
|
*/
|
||||||
|
fun persistentProfile(filesDir: File): File {
|
||||||
|
val stored = File(filesDir, "identity-wg.conf")
|
||||||
|
if (stored.exists() && stored.length() > 0L &&
|
||||||
|
stored.readText().contains("PrivateKey", ignoreCase = true)) {
|
||||||
|
return stored
|
||||||
|
}
|
||||||
|
val fresh = download("/wg/profile/new", "identity-wg.conf.tmp", filesDir)
|
||||||
|
fresh.copyTo(stored, overwrite = true)
|
||||||
|
fresh.delete()
|
||||||
|
return stored
|
||||||
|
}
|
||||||
|
|
||||||
|
/** kbin Tor egress status for the client UI (read-only, kbin-safe). */
|
||||||
|
fun torStatus(): JSONObject? {
|
||||||
|
val c = open("/wg/tor-status")
|
||||||
|
return try {
|
||||||
|
if (c.responseCode !in 200..299) null
|
||||||
|
else JSONObject(c.inputStream.bufferedReader().readText())
|
||||||
|
} catch (_: Exception) { null } finally { c.disconnect() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** R3 tunnel status. Returns (onTunnel, peerIp?). */
|
||||||
|
fun r3Check(): Pair<Boolean, String?> {
|
||||||
|
val c = open("/wg/r3-check")
|
||||||
|
try {
|
||||||
|
if (c.responseCode !in 200..299) return false to null
|
||||||
|
val body = c.inputStream.bufferedReader().readText()
|
||||||
|
val j = JSONObject(body)
|
||||||
|
return j.optBoolean("tunnel", false) to j.optString("peer_ip", null)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return false to null
|
||||||
|
} finally { c.disconnect() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cheap reachability probe for the discover step. */
|
||||||
|
fun reachable(): Boolean = try {
|
||||||
|
val c = open("/wg/r3-check"); val ok = c.responseCode in 200..499; c.disconnect(); ok
|
||||||
|
} catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp" android:height="108dp"
|
||||||
|
android:viewportWidth="108" android:viewportHeight="108">
|
||||||
|
<path android:fillColor="#C9A84C"
|
||||||
|
android:pathData="M54,40c-10,0 -19,6 -24,14c5,8 14,14 24,14s19,-6 24,-14c-5,-8 -14,-14 -24,-14zM54,64a10,10 0 1,1 0,-20a10,10 0 0,1 0,20z" />
|
||||||
|
<path android:fillColor="#00D4FF"
|
||||||
|
android:pathData="M54,49a5,5 0 1,0 0,10a5,5 0 0,0 0,-10z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#0A0A0F</color>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">VILLAGE3B ToolBoX</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.SecuBoxToolBox" parent="android:Theme.Material.NoActionBar">
|
||||||
|
<item name="android:statusBarColor">#0A0A0F</item>
|
||||||
|
<item name="android:navigationBarColor">#0A0A0F</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||||
|
<paths>
|
||||||
|
<cache-path name="cache" path="." />
|
||||||
|
</paths>
|
||||||
5
clients/android-toolbox/build.gradle.kts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
plugins {
|
||||||
|
id("com.android.application") version "8.5.2" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
|
||||||
|
}
|
||||||
5
clients/android-toolbox/gradle.properties
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
android.useAndroidX=true
|
||||||
|
kotlin.code.style=official
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
18
clients/android-toolbox/settings.gradle.kts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// SecuBox-Deb :: Android ToolBox client (#531)
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootProject.name = "SecuBoxToolBox"
|
||||||
|
include(":app")
|
||||||
5
clients/webext-toolbox/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# build artefacts
|
||||||
|
*.xpi
|
||||||
|
*.zip
|
||||||
|
*.crx
|
||||||
|
web-ext-artifacts/
|
||||||
110
clients/webext-toolbox/README.md
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||||
|
# SecuBox ToolBoX — browser extension (Cartographie sociale, #532)
|
||||||
|
|
||||||
|
A WebExtension (Firefox `.xpi` + Chromium MV3) that **emancipates** the R3
|
||||||
|
toolbox live tracker analysis into the browser: instead of only seeing the
|
||||||
|
*cartographie sociale* on `kbin/social/me`, a toolbar badge ticks up as
|
||||||
|
trackers fire, and a popup shows who is watching you — live.
|
||||||
|
|
||||||
|
Sibling of [`clients/android-toolbox/`](../android-toolbox/). Talks **only**
|
||||||
|
to your cabine over the R3 tunnel — no third-party calls.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
- **Pairing** — calls `/social/me` over the tunnel, which 303-redirects to
|
||||||
|
`/social/{token}`; the extension reads the minted HMAC token from the
|
||||||
|
final URL. Anonymous (rotating `mac_hash`), no account. Manual token entry
|
||||||
|
available in the options page.
|
||||||
|
- **Live badge** — the toolbar icon shows the live tracker count for the
|
||||||
|
session (polled once a minute). Colour escalates: gold → 🟥 anti-bot
|
||||||
|
present → 🟪 operator-grade present.
|
||||||
|
- **Popup** — four stat tiles (trackers / sites / anti-bot / operator-grade),
|
||||||
|
a dependency-free **mini Round-Eye graph** (device centre, trackers on the
|
||||||
|
ring, radius by hits, colour by tier), and a top-tracker list with CDN
|
||||||
|
(12.A) / anti-bot (12.B) / operator-grade (12.C) tags.
|
||||||
|
- **Actions** — *Cartographie complète* (opens the full d3 view at
|
||||||
|
`/social/{token}`), *Rapport PDF* (`/social/report/{token}.pdf`), and
|
||||||
|
*Effacer mes données* (RGPD art. 17 wipe → `POST /social/wipe/{token}`).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Published release `.xpi` (downloadable directly):
|
||||||
|
|
||||||
|
```
|
||||||
|
https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.4/secubox-toolbox-webext.xpi
|
||||||
|
```
|
||||||
|
|
||||||
|
The toolbox also serves it from the cabine:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://kbin.<board>.secubox.in/wg/toolbox.xpi
|
||||||
|
```
|
||||||
|
|
||||||
|
The kbin onboard panel exposes a **🧩 Extension navigateur (cartographie)**
|
||||||
|
button. When a local build is present the cabine serves it; otherwise it
|
||||||
|
302-redirects to the **tag-pinned** release asset above. The webext release
|
||||||
|
is published `make_latest:false` so it does not steal the repo "Latest"
|
||||||
|
pointer from the Android APK release (whose endpoint resolves via
|
||||||
|
`/releases/latest/download/…`) — bump the tag in the `/wg/toolbox.xpi`
|
||||||
|
endpoint constant + `secubox-toolbox-fetch-xpi` when a new `webext-v*`
|
||||||
|
release is cut.
|
||||||
|
|
||||||
|
- **Firefox** — open the `.xpi`. A permanent install needs an AMO-signed
|
||||||
|
build (release CI step / `web-ext sign`); for development use
|
||||||
|
*about:debugging → Load Temporary Add-on*, or an ESR/Dev build with
|
||||||
|
`xpinstall.signatures.required=false`.
|
||||||
|
- **Linux Firefox (fast)** — one call grabs the `.xpi` and launches Firefox
|
||||||
|
with it loaded (via `web-ext run`, no signing needed):
|
||||||
|
```bash
|
||||||
|
clients/webext-toolbox/install-firefox-linux.sh # from kbin.gk2.secubox.in
|
||||||
|
clients/webext-toolbox/install-firefox-linux.sh --release # from the GitHub release
|
||||||
|
clients/webext-toolbox/install-firefox-linux.sh --local # from this checkout
|
||||||
|
```
|
||||||
|
- **Chromium** — load unpacked (`chrome://extensions` → Developer mode).
|
||||||
|
Ships rasterised PNG icons (`icons/icon-48/128.png`), so it loads as-is.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
No bundler — the extension is plain JS/HTML/CSS. CI zips it:
|
||||||
|
|
||||||
|
- GitHub Actions `build-webext.yml` → `.xpi` artifact on push to `master` /
|
||||||
|
PRs touching `clients/webext-toolbox/**`; tagging `webext-v*` publishes the
|
||||||
|
`.xpi` as a release asset.
|
||||||
|
|
||||||
|
Locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd clients/webext-toolbox
|
||||||
|
./build.sh # → secubox-toolbox-webext-<version>.xpi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|------|------|
|
||||||
|
| `manifest.json` | MV3, cross-browser background (`service_worker` + `scripts`) |
|
||||||
|
| `api.js` | shared client over `/wg/r3-check`, `/social/*` |
|
||||||
|
| `background.js` | badge sync + silent re-pair (SW or event page) |
|
||||||
|
| `popup/` | live view, mini graph (`graph.js`), actions |
|
||||||
|
| `options/` | host / window / manual token |
|
||||||
|
|
||||||
|
## Cabine endpoints consumed
|
||||||
|
|
||||||
|
| Endpoint | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `/wg/r3-check` | tunnel presence indicator |
|
||||||
|
| `/social/me` | pair → mint token (303 → `/social/{token}`) |
|
||||||
|
| `/social/graph/{token}?since=` | per-session tracker graph JSON |
|
||||||
|
| `/social/wipe/{token}` | RGPD art. 17 erasure |
|
||||||
|
| `/social/{token}` | full d3 cartographie page |
|
||||||
|
| `/social/report/{token}.pdf` | bilingual PDF report |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- No server-side CORS needed: an MV3 extension with `host_permissions` for
|
||||||
|
`*.secubox.in` fetches cross-origin from its background without CORS.
|
||||||
|
- MVP polls `/social/graph` and computes the delta client-side; a future
|
||||||
|
`GET /social/live/{token}` (SSE) can replace the poll. The deception-plane
|
||||||
|
*Poke/Emancipate* per-site control lands once #525 ships.
|
||||||
|
|
||||||
|
License `LicenseRef-CMSD-1.0`.
|
||||||
161
clients/webext-toolbox/api.js
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
//
|
||||||
|
// SecuBox-Deb :: webext-toolbox :: api
|
||||||
|
// Thin client over the R3 toolbox social endpoints. Shared by the
|
||||||
|
// background service worker and the popup. Cross-origin fetches are
|
||||||
|
// allowed because the extension holds host_permissions for the cabine
|
||||||
|
// vhosts — no server-side CORS needed.
|
||||||
|
|
||||||
|
// browser (Firefox promise API) || chrome (Chromium / FF MV3 SW)
|
||||||
|
const ext = globalThis.browser || globalThis.chrome;
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
host: "kbin.gk2.secubox.in",
|
||||||
|
token: "",
|
||||||
|
since: 86400,
|
||||||
|
};
|
||||||
|
|
||||||
|
// base URL from a stored host (accept bare host or full origin)
|
||||||
|
function baseUrl(host) {
|
||||||
|
const h = (host || DEFAULTS.host).trim().replace(/\/+$/, "");
|
||||||
|
if (/^https?:\/\//i.test(h)) return h;
|
||||||
|
return `https://${h}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getConfig() {
|
||||||
|
const stored = await ext.storage.local.get(["host", "token", "since"]);
|
||||||
|
return { ...DEFAULTS, ...stored };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setConfig(patch) {
|
||||||
|
await ext.storage.local.set(patch);
|
||||||
|
return getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the HMAC token from a /social/{token} URL path.
|
||||||
|
function tokenFromUrl(url) {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
const m = u.pathname.match(/\/social\/([^/?#]+)/);
|
||||||
|
if (m && m[1] !== "me" && m[1].split(".").length === 4) return m[1];
|
||||||
|
} catch (_) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pair: hit /social/me over the tunnel; it 303-redirects to
|
||||||
|
// /social/{token}. fetch follows the redirect, so response.url carries
|
||||||
|
// the minted token. Returns the token or throws.
|
||||||
|
async function pair(host) {
|
||||||
|
const url = `${baseUrl(host)}/social/me`;
|
||||||
|
const resp = await fetch(url, { redirect: "follow", credentials: "omit" });
|
||||||
|
const tok = tokenFromUrl(resp.url);
|
||||||
|
if (!tok) throw new Error("pairing failed — not on the R3 tunnel?");
|
||||||
|
return tok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// r3-check: is this client on the R3 tunnel right now?
|
||||||
|
async function r3Check(host) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${baseUrl(host)}/wg/r3-check`, { credentials: "omit" });
|
||||||
|
if (!resp.ok) return { tunnel: false, peer_ip: null };
|
||||||
|
return await resp.json();
|
||||||
|
} catch (_) {
|
||||||
|
return { tunnel: false, peer_ip: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #683 — kbin Tor egress status (public, kbin-safe endpoint).
|
||||||
|
async function torStatus(host) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${baseUrl(host)}/wg/tor-status`, { credentials: "omit" });
|
||||||
|
if (!resp.ok) return { tor_mode: false };
|
||||||
|
return await resp.json();
|
||||||
|
} catch (_) {
|
||||||
|
return { tor_mode: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// graph: the per-session cartographie JSON. Throws on HTTP error so the
|
||||||
|
// caller can show "token expired — re-pair".
|
||||||
|
async function graph(host, token, since) {
|
||||||
|
const qs = new URLSearchParams({ since: String(since || DEFAULTS.since) });
|
||||||
|
const resp = await fetch(`${baseUrl(host)}/social/graph/${token}?${qs}`, {
|
||||||
|
credentials: "omit",
|
||||||
|
});
|
||||||
|
if (resp.status === 403 || resp.status === 404) {
|
||||||
|
throw new Error("token-expired");
|
||||||
|
}
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
return await resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGPD art.17 wipe.
|
||||||
|
async function wipe(host, token) {
|
||||||
|
const resp = await fetch(`${baseUrl(host)}/social/wipe/${token}`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "omit",
|
||||||
|
});
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
return await resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// #574 — protection stats + modular filter toggles (cabine admin API).
|
||||||
|
async function ghost(host) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${baseUrl(host)}/admin/ghost`, { credentials: "omit" });
|
||||||
|
return r.ok ? await r.json() : null;
|
||||||
|
} catch (_) { return null; }
|
||||||
|
}
|
||||||
|
async function getAdminFilters(host) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${baseUrl(host)}/admin/filters`, { credentials: "omit" });
|
||||||
|
return r.ok ? await r.json() : null;
|
||||||
|
} catch (_) { return null; }
|
||||||
|
}
|
||||||
|
async function setAdminFilters(host, patch) {
|
||||||
|
const r = await fetch(`${baseUrl(host)}/admin/filters`, {
|
||||||
|
method: "POST", credentials: "omit",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(patch),
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
|
return await r.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Favicon of a major site/tracker via the cabine's server-side proxy
|
||||||
|
// (7-day cached PNG, transparent 1×1 fallback) — no third-party call.
|
||||||
|
function faviconUrl(host, domain) {
|
||||||
|
return `${baseUrl(host)}/social/favicon/${encodeURIComponent(domain || "")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function socialUrl(host, token) {
|
||||||
|
return `${baseUrl(host)}/social/${token}`;
|
||||||
|
}
|
||||||
|
function reportUrl(host, token) {
|
||||||
|
return `${baseUrl(host)}/social/report/${token}.pdf`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SbxApi = {
|
||||||
|
DEFAULTS,
|
||||||
|
ext,
|
||||||
|
baseUrl,
|
||||||
|
getConfig,
|
||||||
|
setConfig,
|
||||||
|
pair,
|
||||||
|
r3Check,
|
||||||
|
torStatus,
|
||||||
|
graph,
|
||||||
|
wipe,
|
||||||
|
ghost,
|
||||||
|
getAdminFilters,
|
||||||
|
setAdminFilters,
|
||||||
|
faviconUrl,
|
||||||
|
socialUrl,
|
||||||
|
reportUrl,
|
||||||
|
tokenFromUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usable both as a classic background script (globalThis) and an ES-less
|
||||||
|
// service worker. No module syntax to stay loadable as plain script.
|
||||||
|
globalThis.SbxApi = SbxApi;
|
||||||
81
clients/webext-toolbox/background.js
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
//
|
||||||
|
// SecuBox-Deb :: webext-toolbox :: background
|
||||||
|
// Keeps the toolbar badge in sync with the live tracker count and
|
||||||
|
// re-pairs over the R3 tunnel when the token expires. Works as a
|
||||||
|
// Chromium MV3 service worker (importScripts) AND a Firefox event page
|
||||||
|
// (api.js preloaded via background.scripts).
|
||||||
|
|
||||||
|
if (typeof importScripts === "function") {
|
||||||
|
// Chromium service worker: api.js isn't auto-loaded, pull it in.
|
||||||
|
try { importScripts("api.js"); } catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB: do NOT declare `const ext` here — api.js already declares it in the
|
||||||
|
// shared script scope (event page) / worker global (importScripts), and a
|
||||||
|
// second `const ext` is a "redeclaration of const ext" SyntaxError that
|
||||||
|
// kills the whole background script. Use api.ext instead.
|
||||||
|
const api = globalThis.SbxApi;
|
||||||
|
|
||||||
|
const ALARM = "sbx-refresh";
|
||||||
|
const PERIOD_MIN = 1; // poll the cabine once a minute
|
||||||
|
|
||||||
|
function setBadge(text, color) {
|
||||||
|
try {
|
||||||
|
api.ext.action.setBadgeText({ text: text || "" });
|
||||||
|
if (color) ext.action.setBadgeBackgroundColor({ color });
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the graph, update the badge with the live tracker count. Auto
|
||||||
|
// re-pairs once if the stored token has expired.
|
||||||
|
async function refresh() {
|
||||||
|
const cfg = await api.getConfig();
|
||||||
|
if (!cfg.host) { setBadge("", "#6b6b7a"); return; }
|
||||||
|
|
||||||
|
let token = cfg.token;
|
||||||
|
const run = async (tok) => api.graph(cfg.host, tok, cfg.since);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!token) token = await api.pair(cfg.host);
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await run(token);
|
||||||
|
} catch (e) {
|
||||||
|
if (String(e.message) === "token-expired") {
|
||||||
|
token = await api.pair(cfg.host); // one silent re-pair
|
||||||
|
data = await run(token);
|
||||||
|
} else throw e;
|
||||||
|
}
|
||||||
|
await api.setConfig({ token });
|
||||||
|
const n = (data.stats && data.stats.total_trackers) || 0;
|
||||||
|
// colour escalates with operator-grade / anti-bot presence
|
||||||
|
const opg = (data.stats && data.stats.opgrade_sites) || 0;
|
||||||
|
const ab = (data.stats && data.stats.antibot_sites) || 0;
|
||||||
|
const color = opg > 0 ? "#6e40c9" : ab > 0 ? "#e63946" : "#c9a84c";
|
||||||
|
setBadge(n > 999 ? "999+" : String(n), color);
|
||||||
|
await ext.storage.local.set({ lastStats: data.stats || {}, lastError: "" });
|
||||||
|
} catch (e) {
|
||||||
|
setBadge("!", "#6b6b7a");
|
||||||
|
await ext.storage.local.set({ lastError: String(e.message || e) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.ext.runtime.onInstalled.addListener(() => {
|
||||||
|
api.ext.alarms.create(ALARM, { periodInMinutes: PERIOD_MIN });
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
api.ext.runtime.onStartup && api.ext.runtime.onStartup.addListener(() => {
|
||||||
|
api.ext.alarms.create(ALARM, { periodInMinutes: PERIOD_MIN });
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
api.ext.alarms.onAlarm.addListener((a) => { if (a.name === ALARM) refresh(); });
|
||||||
|
|
||||||
|
// popup asks for an immediate refresh after pairing / config change
|
||||||
|
api.ext.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||||
|
if (msg && msg.type === "refresh") {
|
||||||
|
refresh().then(() => sendResponse({ ok: true }));
|
||||||
|
return true; // async response
|
||||||
|
}
|
||||||
|
});
|
||||||
26
clients/webext-toolbox/build.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
#
|
||||||
|
# SecuBox ToolBoX Cartographie — build the unsigned .xpi (a zip of the
|
||||||
|
# extension dir). Firefox loads it as-is (temporary add-on / ESR with
|
||||||
|
# signatures off) ; a release build signs it via web-ext / AMO.
|
||||||
|
# Usage: ./build.sh → produces ./secubox-toolbox-webext-<version>.xpi
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
VER=$(grep -oE '"version"[^,]*' manifest.json | grep -oE '[0-9.]+' | head -1)
|
||||||
|
OUT="secubox-toolbox-webext-${VER}.xpi"
|
||||||
|
rm -f "$OUT"
|
||||||
|
|
||||||
|
# -FS = sync (drop stale entries) ; exclude VCS, dotfiles, build script,
|
||||||
|
# any previously built artefact, docs, and the SVG icon source (only the
|
||||||
|
# rasterised PNGs are referenced by the manifest — keep SVG out of the
|
||||||
|
# package so Firefox never renders it in chrome UI).
|
||||||
|
zip -r -FS "$OUT" . \
|
||||||
|
-x '*.git*' '*/.*' 'build.sh' '*.xpi' 'README.md' 'icons/icon.svg' >/dev/null
|
||||||
|
|
||||||
|
echo "built $OUT ($(stat -c%s "$OUT" 2>/dev/null || stat -f%z "$OUT") bytes)"
|
||||||
|
echo "Firefox: about:debugging → This Firefox → Load Temporary Add-on → pick the .xpi (or manifest.json)."
|
||||||
|
echo "Permanent install needs signing (web-ext sign / AMO) or Dev/ESR with xpinstall.signatures.required=false."
|
||||||
|
echo "Chromium: action icons must be raster — rasterise icons/icon.svg to PNG before a Chromium store build."
|
||||||
BIN
clients/webext-toolbox/icons/icon-128.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
clients/webext-toolbox/icons/icon-48.png
Normal file
|
After Width: | Height: | Size: 618 B |
22
clients/webext-toolbox/icons/icon.svg
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<rect width="128" height="128" rx="24" fill="#0a0a0f"/>
|
||||||
|
<!-- outer eye almond -->
|
||||||
|
<path d="M64 36 C92 36 114 64 114 64 C114 64 92 92 64 92 C36 92 14 64 14 64 C14 64 36 36 64 36 Z"
|
||||||
|
fill="none" stroke="#c9a84c" stroke-width="5"/>
|
||||||
|
<!-- iris -->
|
||||||
|
<circle cx="64" cy="64" r="20" fill="#0c0c12" stroke="#00ff41" stroke-width="4"/>
|
||||||
|
<circle cx="64" cy="64" r="7" fill="#00ff41"/>
|
||||||
|
<!-- tracker spokes -->
|
||||||
|
<g stroke="#6e40c9" stroke-width="3" opacity="0.8">
|
||||||
|
<line x1="64" y1="64" x2="104" y2="40"/>
|
||||||
|
<line x1="64" y1="64" x2="24" y2="40"/>
|
||||||
|
<line x1="64" y1="64" x2="100" y2="92"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#00d4ff">
|
||||||
|
<circle cx="104" cy="40" r="5"/>
|
||||||
|
<circle cx="24" cy="40" r="5"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="100" cy="92" r="5" fill="#e63946"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 951 B |
87
clients/webext-toolbox/install-firefox-linux.sh
Executable file
|
|
@ -0,0 +1,87 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
#
|
||||||
|
# SecuBox ToolBoX — Linux Firefox installer (#547)
|
||||||
|
# One call: grab the ToolBoX cartographie extension and launch Firefox with
|
||||||
|
# it loaded. Prefers `web-ext run` (temporary load, works unsigned — fastest)
|
||||||
|
# and falls back to opening the .xpi for the install prompt.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./install-firefox-linux.sh # from kbin.gk2.secubox.in
|
||||||
|
# ./install-firefox-linux.sh kbin.my.box # from another cabine host
|
||||||
|
# ./install-firefox-linux.sh --release # from the latest GitHub release
|
||||||
|
# ./install-firefox-linux.sh --local # build from this checkout (web-ext)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEFAULT_HOST="kbin.gk2.secubox.in"
|
||||||
|
RELEASE_URL="https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.4/secubox-toolbox-webext.xpi"
|
||||||
|
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
say(){ printf '\033[1;36m▸\033[0m %s\n' "$*"; }
|
||||||
|
warn(){ printf '\033[1;33m!\033[0m %s\n' "$*" >&2; }
|
||||||
|
die(){ printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ── resolve source ──
|
||||||
|
MODE="host"; HOST="$DEFAULT_HOST"; SRC_URL=""
|
||||||
|
case "${1:-}" in
|
||||||
|
--release) MODE="release"; SRC_URL="$RELEASE_URL" ;;
|
||||||
|
--local) MODE="local" ;;
|
||||||
|
"") SRC_URL="https://${HOST}/wg/toolbox.xpi" ;;
|
||||||
|
-*) die "unknown flag: $1 (use --release | --local | <host>)" ;;
|
||||||
|
*) HOST="$1"; SRC_URL="https://${HOST}/wg/toolbox.xpi" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ── find a Firefox binary ──
|
||||||
|
FX=""
|
||||||
|
for c in firefox firefox-esr firefox-bin firefox-developer-edition; do
|
||||||
|
if command -v "$c" >/dev/null 2>&1; then FX="$c"; break; fi
|
||||||
|
done
|
||||||
|
if [ -z "$FX" ] && command -v flatpak >/dev/null 2>&1 \
|
||||||
|
&& flatpak info org.mozilla.firefox >/dev/null 2>&1; then
|
||||||
|
FX="flatpak run org.mozilla.firefox"
|
||||||
|
fi
|
||||||
|
[ -n "$FX" ] || die "no Firefox found (install firefox / firefox-esr, or flatpak org.mozilla.firefox)"
|
||||||
|
say "Firefox: $FX"
|
||||||
|
|
||||||
|
have_webext(){ command -v web-ext >/dev/null 2>&1 || command -v npx >/dev/null 2>&1; }
|
||||||
|
runwebext(){ if command -v web-ext >/dev/null 2>&1; then web-ext "$@"; else npx --yes web-ext "$@"; fi; }
|
||||||
|
|
||||||
|
# ── fastest path: web-ext run (temporary load, no signing needed) ──
|
||||||
|
if have_webext; then
|
||||||
|
SRCDIR=""
|
||||||
|
if [ "$MODE" = "local" ]; then
|
||||||
|
[ -f "$SELF_DIR/manifest.json" ] || die "--local: no manifest.json next to this script"
|
||||||
|
SRCDIR="$SELF_DIR"
|
||||||
|
else
|
||||||
|
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
||||||
|
say "Downloading extension from ${SRC_URL} …"
|
||||||
|
curl -fsSL "$SRC_URL" -o "$TMP/sbx.xpi" || die "download failed: $SRC_URL"
|
||||||
|
head -c2 "$TMP/sbx.xpi" | grep -q PK || die "not a valid .xpi (zip) — wrong host/URL?"
|
||||||
|
mkdir -p "$TMP/ext" && ( cd "$TMP/ext" && unzip -q "$TMP/sbx.xpi" )
|
||||||
|
SRCDIR="$TMP/ext"
|
||||||
|
fi
|
||||||
|
say "Launching Firefox with the ToolBoX extension loaded (temporary)…"
|
||||||
|
FXBIN="${FX%% *}" # web-ext wants the binary, not a flatpak wrapper
|
||||||
|
if [ "$FX" = "${FX# }" ] && command -v "$FXBIN" >/dev/null 2>&1; then
|
||||||
|
exec runwebext run --source-dir "$SRCDIR" --firefox "$FXBIN" \
|
||||||
|
--start-url "https://${HOST}/social/me"
|
||||||
|
fi
|
||||||
|
exec runwebext run --source-dir "$SRCDIR" --start-url "https://${HOST}/social/me"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── fallback: open the .xpi so Firefox shows the install prompt ──
|
||||||
|
warn "web-ext not found (no npx) — falling back to the install prompt."
|
||||||
|
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
||||||
|
[ "$MODE" = "local" ] && die "--local needs web-ext/npx; install nodejs or use a host/--release"
|
||||||
|
say "Downloading ${SRC_URL} …"
|
||||||
|
curl -fsSL "$SRC_URL" -o "$TMP/secubox-toolbox-webext.xpi" || die "download failed"
|
||||||
|
head -c2 "$TMP/secubox-toolbox-webext.xpi" | grep -q PK || die "not a valid .xpi"
|
||||||
|
cat <<'NOTE'
|
||||||
|
! The .xpi is unsigned. Stock Firefox release refuses a permanent install.
|
||||||
|
Use Firefox ESR/Developer/Nightly, or set in about:config:
|
||||||
|
xpinstall.signatures.required = false
|
||||||
|
…then accept the install prompt that opens now.
|
||||||
|
NOTE
|
||||||
|
say "Opening Firefox on the extension…"
|
||||||
|
exec $FX "$TMP/secubox-toolbox-webext.xpi"
|
||||||
36
clients/webext-toolbox/manifest.json
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "SecuBox ToolBoX — Cartographie sociale",
|
||||||
|
"version": "0.1.5",
|
||||||
|
"description": "Surface the SecuBox R3 toolbox live tracker analysis (cartographie sociale) in your browser: live badge, per-session trackers, mini Round-Eye graph, RGPD wipe + PDF report.",
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "secubox-toolbox-webext@cybermind.fr",
|
||||||
|
"strict_min_version": "115.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": ["storage", "alarms"],
|
||||||
|
"host_permissions": [
|
||||||
|
"*://*.secubox.in/*"
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"default_title": "SecuBox Cartographie",
|
||||||
|
"default_popup": "popup/popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"48": "icons/icon-48.png",
|
||||||
|
"128": "icons/icon-128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"48": "icons/icon-48.png",
|
||||||
|
"128": "icons/icon-128.png"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js",
|
||||||
|
"scripts": ["api.js", "background.js"]
|
||||||
|
},
|
||||||
|
"options_ui": {
|
||||||
|
"page": "options/options.html",
|
||||||
|
"open_in_tab": true
|
||||||
|
}
|
||||||
|
}
|
||||||
45
clients/webext-toolbox/options/options.html
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>SecuBox Cartographie — Réglages</title>
|
||||||
|
<style>
|
||||||
|
body { background:#0a0a0f; color:#e8e6d9; font:14px/1.5 system-ui,sans-serif;
|
||||||
|
max-width:520px; margin:40px auto; padding:0 20px; }
|
||||||
|
h1 { color:#c9a84c; font-size:18px; }
|
||||||
|
label { display:block; color:#6b6b7a; font-size:12px; margin:14px 0 4px; }
|
||||||
|
input,select { width:100%; padding:8px; border-radius:6px; border:1px solid #333;
|
||||||
|
background:#14141c; color:#e8e6d9; }
|
||||||
|
button { margin-top:16px; padding:9px 14px; border-radius:6px; border:1px solid #c9a84c;
|
||||||
|
background:#c9a84c; color:#0a0a0f; font-weight:700; cursor:pointer; }
|
||||||
|
.muted { color:#6b6b7a; font-size:12px; }
|
||||||
|
#msg { color:#00ff41; min-height:18px; margin-top:10px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>👁️ SecuBox Cartographie — Réglages</h1>
|
||||||
|
<p class="muted">L'extension parle uniquement à ta cabine via le tunnel R3.
|
||||||
|
Aucune donnée n'est envoyée ailleurs.</p>
|
||||||
|
|
||||||
|
<label>Borne (hôte de la cabine)
|
||||||
|
<input id="host" type="text" placeholder="kbin.gk2.secubox.in" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
<label>Fenêtre d'analyse
|
||||||
|
<select id="since">
|
||||||
|
<option value="3600">1 heure</option>
|
||||||
|
<option value="86400" selected>24 heures</option>
|
||||||
|
<option value="604800">7 jours</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Jeton de session (optionnel — sinon appairage auto via R3)
|
||||||
|
<input id="token" type="text" placeholder="mac.exp.nonce.sig" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button id="save">Enregistrer</button>
|
||||||
|
<p id="msg"></p>
|
||||||
|
|
||||||
|
<script src="../api.js"></script>
|
||||||
|
<script src="options.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
clients/webext-toolbox/options/options.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
const api = globalThis.SbxApi;
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const cfg = await api.getConfig();
|
||||||
|
$("host").value = cfg.host;
|
||||||
|
$("token").value = cfg.token || "";
|
||||||
|
$("since").value = String(cfg.since);
|
||||||
|
}
|
||||||
|
|
||||||
|
$("save").addEventListener("click", async () => {
|
||||||
|
await api.setConfig({
|
||||||
|
host: $("host").value.trim() || api.DEFAULTS.host,
|
||||||
|
token: $("token").value.trim(),
|
||||||
|
since: parseInt($("since").value, 10) || api.DEFAULTS.since,
|
||||||
|
});
|
||||||
|
api.ext.runtime.sendMessage({ type: "refresh" });
|
||||||
|
$("msg").textContent = "Enregistré ✓";
|
||||||
|
setTimeout(() => ($("msg").textContent = ""), 1500);
|
||||||
|
});
|
||||||
|
|
||||||
|
load();
|
||||||
80
clients/webext-toolbox/popup/graph.js
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
//
|
||||||
|
// Dependency-free mini "Round-Eye" cartographie : the device at the
|
||||||
|
// centre, top trackers on an outer ring, radius/colour by hits + tier.
|
||||||
|
// A compact stand-in for the full d3 view served at /social/{token}.
|
||||||
|
|
||||||
|
const SVGNS = "http://www.w3.org/2000/svg";
|
||||||
|
const PAL = {
|
||||||
|
base: "#c9a84c", // gold
|
||||||
|
cdn: "#00d4ff", // cyan
|
||||||
|
ab: "#e63946", // cinnabar (anti-bot)
|
||||||
|
opg: "#6e40c9", // void purple (operator-grade)
|
||||||
|
eye: "#00ff41", // matrix green
|
||||||
|
link: "#2a2a3a",
|
||||||
|
};
|
||||||
|
|
||||||
|
function el(name, attrs) {
|
||||||
|
const n = document.createElementNS(SVGNS, name);
|
||||||
|
for (const k in attrs) n.setAttribute(k, attrs[k]);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tierOf(node) {
|
||||||
|
if (node.opgrade_vendor) return "opg";
|
||||||
|
if (node.antibot_vendor) return "ab";
|
||||||
|
if (node.cdn_vendor) return "cdn";
|
||||||
|
return "base";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGraph(svg, data) {
|
||||||
|
while (svg.firstChild) svg.removeChild(svg.firstChild);
|
||||||
|
const W = 260, H = 180, cx = W / 2, cy = H / 2;
|
||||||
|
|
||||||
|
const nodes = (data && data.nodes ? data.nodes.slice() : [])
|
||||||
|
.sort((a, b) => (b.hits || 0) - (a.hits || 0))
|
||||||
|
.slice(0, 14);
|
||||||
|
|
||||||
|
if (!nodes.length) {
|
||||||
|
const t = el("text", { x: cx, y: cy, fill: "#6b6b7a", "font-size": 11,
|
||||||
|
"text-anchor": "middle" });
|
||||||
|
t.textContent = "Aucun traceur détecté pour l'instant";
|
||||||
|
svg.appendChild(t);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxHits = Math.max(...nodes.map((n) => n.hits || 1));
|
||||||
|
const R = 66;
|
||||||
|
|
||||||
|
// spokes first (under the dots)
|
||||||
|
nodes.forEach((n, i) => {
|
||||||
|
const a = (i / nodes.length) * Math.PI * 2 - Math.PI / 2;
|
||||||
|
const x = cx + Math.cos(a) * R, y = cy + Math.sin(a) * R;
|
||||||
|
svg.appendChild(el("line", { x1: cx, y1: cy, x2: x, y2: y,
|
||||||
|
stroke: PAL.link, "stroke-width": 1 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// tracker dots
|
||||||
|
nodes.forEach((n, i) => {
|
||||||
|
const a = (i / nodes.length) * Math.PI * 2 - Math.PI / 2;
|
||||||
|
const x = cx + Math.cos(a) * R, y = cy + Math.sin(a) * R;
|
||||||
|
const r = 3 + Math.round(6 * Math.sqrt((n.hits || 1) / maxHits));
|
||||||
|
const fill = PAL[tierOf(n)];
|
||||||
|
const c = el("circle", { cx: x, cy: y, r, fill, "fill-opacity": 0.85 });
|
||||||
|
const title = el("title", {});
|
||||||
|
title.textContent = `${n.domain} — ${n.hits || 0} hits`
|
||||||
|
+ (n.cdn_vendor ? ` · ${n.cdn_vendor}` : "")
|
||||||
|
+ (n.antibot_vendor ? ` · anti-bot ${n.antibot_vendor}` : "")
|
||||||
|
+ (n.opgrade_vendor ? ` · opérateur ${n.opgrade_vendor}` : "");
|
||||||
|
c.appendChild(title);
|
||||||
|
svg.appendChild(c);
|
||||||
|
});
|
||||||
|
|
||||||
|
// the eye (device) at the centre
|
||||||
|
svg.appendChild(el("circle", { cx, cy, r: 13, fill: "#0c0c12",
|
||||||
|
stroke: PAL.eye, "stroke-width": 2 }));
|
||||||
|
svg.appendChild(el("circle", { cx, cy, r: 4.5, fill: PAL.eye }));
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.renderGraph = renderGraph;
|
||||||
92
clients/webext-toolbox/popup/popup.css
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
/* SPDX-License-Identifier: LicenseRef-CMSD-1.0 */
|
||||||
|
/* SecuBox cyberpunk/hermetic palette (DESIGN-CHARTER) */
|
||||||
|
:root {
|
||||||
|
--cosmos: #0a0a0f;
|
||||||
|
--gold: #c9a84c;
|
||||||
|
--cinnabar: #e63946;
|
||||||
|
--matrix: #00ff41;
|
||||||
|
--void: #6e40c9;
|
||||||
|
--cyan: #00d4ff;
|
||||||
|
--text: #e8e6d9;
|
||||||
|
--muted: #6b6b7a;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
width: 300px;
|
||||||
|
margin: 0;
|
||||||
|
background: var(--cosmos);
|
||||||
|
color: var(--text);
|
||||||
|
font: 13px/1.4 system-ui, "Segoe UI", sans-serif;
|
||||||
|
padding: 10px 12px 8px;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.logo { color: var(--gold); font-weight: 700; letter-spacing: .5px; }
|
||||||
|
.r3 {
|
||||||
|
font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
}
|
||||||
|
.r3.on { color: var(--matrix); }
|
||||||
|
.r3.off { color: var(--muted); }
|
||||||
|
.muted { color: var(--muted); font-size: 11px; }
|
||||||
|
.err { color: var(--cinnabar); font-size: 11px; min-height: 14px; }
|
||||||
|
|
||||||
|
label { display: block; font-size: 11px; color: var(--muted); margin: 8px 0 4px; }
|
||||||
|
input[type=text] {
|
||||||
|
width: 100%; padding: 7px 8px; border-radius: 6px;
|
||||||
|
border: 1px solid #333; background: #14141c; color: var(--text);
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
cursor: pointer; border: 1px solid #333; border-radius: 6px;
|
||||||
|
background: #14141c; color: var(--text); padding: 7px 8px; font-size: 12px;
|
||||||
|
}
|
||||||
|
button:hover { border-color: var(--gold); }
|
||||||
|
button.go {
|
||||||
|
width: 100%; margin-top: 8px; background: var(--gold); color: var(--cosmos);
|
||||||
|
font-weight: 700; border-color: var(--gold);
|
||||||
|
}
|
||||||
|
button.danger { color: var(--cinnabar); border-color: var(--cinnabar); }
|
||||||
|
|
||||||
|
.stats { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 6px; margin-bottom: 8px; }
|
||||||
|
.stat {
|
||||||
|
background: #12121a; border: 1px solid #222; border-radius: 6px;
|
||||||
|
padding: 6px 2px; text-align: center;
|
||||||
|
}
|
||||||
|
.stat b { display: block; font-size: 16px; color: var(--gold); }
|
||||||
|
.stat span { font-size: 9px; color: var(--muted); }
|
||||||
|
.stat.warn b { color: var(--cinnabar); }
|
||||||
|
.stat.opg b { color: var(--void); }
|
||||||
|
|
||||||
|
#graph { width: 100%; height: 180px; background: #0c0c12; border-radius: 8px; display: block; }
|
||||||
|
|
||||||
|
.toplist { margin: 8px 0; max-height: 132px; overflow-y: auto; }
|
||||||
|
.row {
|
||||||
|
display: flex; align-items: center; gap: 6px; padding: 3px 2px;
|
||||||
|
border-bottom: 1px solid #1a1a22; font-size: 11px;
|
||||||
|
}
|
||||||
|
.row .fav { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; background: #1a1a22; object-fit: contain; }
|
||||||
|
.row .dom { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.row .hits { color: var(--muted); }
|
||||||
|
|
||||||
|
/* #574 — protection panel */
|
||||||
|
#protect { margin: 8px 0; padding: 8px; background: #0e0e15; border: 1px solid #222; border-radius: 8px; }
|
||||||
|
.phead { color: var(--matrix); font-weight: 700; font-size: 12px; margin-bottom: 6px; }
|
||||||
|
.gstat { color: var(--muted); font-weight: 400; font-size: 10px; }
|
||||||
|
.tg { display: flex; align-items: center; gap: 6px; font-size: 11px; padding: 3px 0; }
|
||||||
|
.tg select { margin-left: auto; background: #14141c; color: var(--text); border: 1px solid #333; border-radius: 4px; }
|
||||||
|
#protect input { accent-color: var(--void); }
|
||||||
|
.tier { font-size: 9px; padding: 1px 4px; border-radius: 3px; }
|
||||||
|
.tier.cdn { background: #1d2a33; color: var(--cyan); }
|
||||||
|
.tier.ab { background: #2a1416; color: var(--cinnabar); }
|
||||||
|
.tier.opg { background: #1e1430; color: var(--void); }
|
||||||
|
|
||||||
|
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 6px 0; }
|
||||||
|
.actions button:last-child { grid-column: 1 / 3; }
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
margin-top: 6px; padding-top: 6px; border-top: 1px solid #1a1a22;
|
||||||
|
}
|
||||||
|
footer a { color: var(--cyan); text-decoration: none; font-size: 11px; }
|
||||||
68
clients/webext-toolbox/popup/popup.html
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="popup.css">
|
||||||
|
<title>SecuBox Cartographie</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<span class="logo">👁️ VILLAGE3B</span>
|
||||||
|
<span id="tordot" class="r3 off" title="Mode Tor" style="display:none">🧅</span>
|
||||||
|
<span id="r3dot" class="r3 off" title="État du tunnel R3">R3</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Pairing (shown when no token) -->
|
||||||
|
<section id="pair" hidden>
|
||||||
|
<p class="muted">Connecte-toi à la cabine pour voir qui t'observe.</p>
|
||||||
|
<label>Borne
|
||||||
|
<input id="host" type="text" placeholder="kbin.gk2.secubox.in" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
<button id="pairBtn" class="go">Appairer (R3)</button>
|
||||||
|
<p id="pairMsg" class="err"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Live view (shown when paired) -->
|
||||||
|
<section id="live" hidden>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat"><b id="sTrackers">–</b><span>traceurs</span></div>
|
||||||
|
<div class="stat"><b id="sSites">–</b><span>sites</span></div>
|
||||||
|
<div class="stat warn"><b id="sAntibot">–</b><span>anti-bot</span></div>
|
||||||
|
<div class="stat opg"><b id="sOpgrade">–</b><span>opérateur</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svg id="graph" viewBox="0 0 260 180" role="img" aria-label="Mini cartographie"></svg>
|
||||||
|
|
||||||
|
<div class="toplist" id="topList"></div>
|
||||||
|
|
||||||
|
<section id="protect">
|
||||||
|
<div class="phead">🛡 Protection <span id="ghostStat" class="gstat"></span></div>
|
||||||
|
<label class="tg"><input type="checkbox" data-f="ad_ghost"> Masquer pubs/bannières (R3+)</label>
|
||||||
|
<label class="tg"><input type="checkbox" data-f="ad_ghost_block"> Bloquer hôtes pub (économie)</label>
|
||||||
|
<label class="tg"><input type="checkbox" data-f="banner"> Bannière transparence</label>
|
||||||
|
<label class="tg">Mode protecteur
|
||||||
|
<select data-f="protective"><option value="off">off</option><option value="alert">alert</option><option value="spoof">spoof</option></select>
|
||||||
|
</label>
|
||||||
|
<p id="protectMsg" class="muted"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button id="openFull">🗺️ Cartographie complète</button>
|
||||||
|
<button id="pdf">📄 Rapport PDF</button>
|
||||||
|
<button id="wipe" class="danger">🧹 Effacer mes données</button>
|
||||||
|
</div>
|
||||||
|
<p id="liveMsg" class="muted"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<a href="#" id="settings">Réglages</a>
|
||||||
|
<span class="muted" id="ver"></span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="../api.js"></script>
|
||||||
|
<script src="graph.js"></script>
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
194
clients/webext-toolbox/popup/popup.js
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
//
|
||||||
|
// SecuBox-Deb :: webext-toolbox :: popup controller
|
||||||
|
|
||||||
|
// NB: api.js (loaded first in this page) already declares `const ext` in the
|
||||||
|
// shared script scope — re-declaring it here is a "redeclaration of const ext"
|
||||||
|
// SyntaxError that aborts popup.js. Use api.ext instead.
|
||||||
|
const api = globalThis.SbxApi;
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
let curHost = api.DEFAULTS.host; // for favicon URLs (#555)
|
||||||
|
|
||||||
|
function show(which) {
|
||||||
|
$("pair").hidden = which !== "pair";
|
||||||
|
$("live").hidden = which !== "live";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillTopList(nodes) {
|
||||||
|
const list = $("topList");
|
||||||
|
list.innerHTML = "";
|
||||||
|
(nodes || [])
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => (b.hits || 0) - (a.hits || 0))
|
||||||
|
.slice(0, 5)
|
||||||
|
.forEach((n) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "row";
|
||||||
|
// favicon of the major site/tracker (cabine proxy) — not an IP (#555)
|
||||||
|
const fav = document.createElement("img");
|
||||||
|
fav.className = "fav";
|
||||||
|
fav.loading = "lazy";
|
||||||
|
fav.alt = "";
|
||||||
|
fav.src = api.faviconUrl(curHost, n.domain || n.id);
|
||||||
|
fav.addEventListener("error", () => { fav.style.visibility = "hidden"; });
|
||||||
|
row.appendChild(fav);
|
||||||
|
const dom = document.createElement("span");
|
||||||
|
dom.className = "dom";
|
||||||
|
dom.textContent = n.domain || n.id;
|
||||||
|
row.appendChild(dom);
|
||||||
|
if (n.opgrade_vendor) addTier(row, "opg", n.opgrade_vendor);
|
||||||
|
else if (n.antibot_vendor) addTier(row, "ab", n.antibot_vendor);
|
||||||
|
else if (n.cdn_vendor) addTier(row, "cdn", n.cdn_vendor);
|
||||||
|
const hits = document.createElement("span");
|
||||||
|
hits.className = "hits";
|
||||||
|
hits.textContent = (n.hits || 0) + "×";
|
||||||
|
row.appendChild(hits);
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function addTier(row, cls, label) {
|
||||||
|
const t = document.createElement("span");
|
||||||
|
t.className = "tier " + cls;
|
||||||
|
t.textContent = label;
|
||||||
|
row.appendChild(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function paint(data) {
|
||||||
|
const s = data.stats || {};
|
||||||
|
$("sTrackers").textContent = s.total_trackers ?? 0;
|
||||||
|
$("sSites").textContent = s.total_sites ?? 0;
|
||||||
|
$("sAntibot").textContent = s.antibot_sites ?? 0;
|
||||||
|
$("sOpgrade").textContent = s.opgrade_sites ?? 0;
|
||||||
|
globalThis.renderGraph($("graph"), data);
|
||||||
|
fillTopList(data.nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #574 — protection stats + live filter toggles in the popup.
|
||||||
|
async function loadProtection() {
|
||||||
|
const sec = $("protect");
|
||||||
|
if (!sec) return;
|
||||||
|
const g = await api.ghost(curHost);
|
||||||
|
if (g) {
|
||||||
|
$("ghostStat").textContent =
|
||||||
|
`${g.blocked_requests || 0} bloqués · ~${g.mb_saved_est || 0} Mo · ${g.pages_cleaned || 0} nettoyées`;
|
||||||
|
}
|
||||||
|
const f = await api.getAdminFilters(curHost);
|
||||||
|
if (!f) { sec.style.opacity = "0.5"; return; }
|
||||||
|
sec.style.opacity = "1";
|
||||||
|
sec.querySelectorAll("[data-f]").forEach((el) => {
|
||||||
|
const k = el.dataset.f;
|
||||||
|
if (el.type === "checkbox") el.checked = !!f[k];
|
||||||
|
else el.value = f[k];
|
||||||
|
});
|
||||||
|
if (!sec.dataset.wired) {
|
||||||
|
sec.dataset.wired = "1";
|
||||||
|
sec.querySelectorAll("[data-f]").forEach((el) => {
|
||||||
|
el.addEventListener("change", async () => {
|
||||||
|
const v = el.type === "checkbox" ? el.checked : el.value;
|
||||||
|
try {
|
||||||
|
await api.setAdminFilters(curHost, { [el.dataset.f]: v });
|
||||||
|
$("protectMsg").textContent = "✓ appliqué";
|
||||||
|
setTimeout(() => ($("protectMsg").textContent = ""), 1000);
|
||||||
|
loadProtection();
|
||||||
|
} catch (e) {
|
||||||
|
$("protectMsg").textContent = "erreur : " + e.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const cfg = await api.getConfig();
|
||||||
|
curHost = cfg.host || api.DEFAULTS.host;
|
||||||
|
$("ver").textContent = "v" + (api.ext.runtime.getManifest().version || "");
|
||||||
|
|
||||||
|
// tunnel indicator
|
||||||
|
api.r3Check(cfg.host).then((r) => {
|
||||||
|
const dot = $("r3dot");
|
||||||
|
dot.className = "r3 " + (r.tunnel ? "on" : "off");
|
||||||
|
dot.title = r.tunnel ? `Tunnel R3 actif (${r.peer_ip || "?"})` : "Hors tunnel R3";
|
||||||
|
});
|
||||||
|
|
||||||
|
// #683 — Tor egress indicator (only visible when kbin Tor mode is on)
|
||||||
|
api.torStatus(cfg.host).then((t) => {
|
||||||
|
const dot = $("tordot");
|
||||||
|
if (!dot) return;
|
||||||
|
if (t && t.tor_mode) {
|
||||||
|
dot.style.display = "";
|
||||||
|
dot.className = "r3 " + (t.running ? "on" : "off");
|
||||||
|
dot.title = t.running
|
||||||
|
? `Mode Tor actif — sortie anonymisée${t.exit_ip ? " (" + t.exit_ip + ")" : ""}`
|
||||||
|
: "Mode Tor activé — démarrage du tunnel…";
|
||||||
|
} else {
|
||||||
|
dot.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cfg.token) {
|
||||||
|
$("host").value = cfg.host;
|
||||||
|
show("pair");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
show("live");
|
||||||
|
$("liveMsg").textContent = "Chargement…";
|
||||||
|
try {
|
||||||
|
const data = await api.graph(cfg.host, cfg.token, cfg.since);
|
||||||
|
paint(data);
|
||||||
|
$("liveMsg").textContent = "";
|
||||||
|
loadProtection();
|
||||||
|
} catch (e) {
|
||||||
|
if (String(e.message) === "token-expired") {
|
||||||
|
// token died — drop it and go back to pairing
|
||||||
|
await api.setConfig({ token: "" });
|
||||||
|
show("pair");
|
||||||
|
$("host").value = cfg.host;
|
||||||
|
$("pairMsg").textContent = "Session expirée — ré-appaire.";
|
||||||
|
} else {
|
||||||
|
$("liveMsg").textContent = "Erreur : " + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── events ──
|
||||||
|
$("pairBtn").addEventListener("click", async () => {
|
||||||
|
const host = $("host").value.trim() || api.DEFAULTS.host;
|
||||||
|
$("pairMsg").textContent = "Appairage…";
|
||||||
|
try {
|
||||||
|
await api.setConfig({ host });
|
||||||
|
const token = await api.pair(host);
|
||||||
|
await api.setConfig({ token });
|
||||||
|
api.ext.runtime.sendMessage({ type: "refresh" });
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
$("pairMsg").textContent = e.message + " (es-tu sur le tunnel ?)";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("openFull").addEventListener("click", async () => {
|
||||||
|
const cfg = await api.getConfig();
|
||||||
|
api.ext.tabs.create({ url: api.socialUrl(cfg.host, cfg.token) });
|
||||||
|
});
|
||||||
|
$("pdf").addEventListener("click", async () => {
|
||||||
|
const cfg = await api.getConfig();
|
||||||
|
api.ext.tabs.create({ url: api.reportUrl(cfg.host, cfg.token) });
|
||||||
|
});
|
||||||
|
$("wipe").addEventListener("click", async () => {
|
||||||
|
if (!confirm("Effacer toutes tes données de cartographie sur la cabine ?")) return;
|
||||||
|
const cfg = await api.getConfig();
|
||||||
|
try {
|
||||||
|
const r = await api.wipe(cfg.host, cfg.token);
|
||||||
|
$("liveMsg").textContent = `Effacé : ${r.rows_deleted ?? 0} entrées.`;
|
||||||
|
await api.setConfig({ token: "" });
|
||||||
|
setTimeout(load, 800);
|
||||||
|
} catch (e) {
|
||||||
|
$("liveMsg").textContent = "Erreur effacement : " + e.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$("settings").addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
api.ext.runtime.openOptionsPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
load();
|
||||||
|
|
@ -55,4 +55,13 @@ server {
|
||||||
proxy_pass http://unix:/run/secubox/system.sock:/;
|
proxy_pass http://unix:/run/secubox/system.sock:/;
|
||||||
include /etc/nginx/snippets/secubox-proxy.conf;
|
include /etc/nginx/snippets/secubox-proxy.conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# #65: per-module routes self-register here. Every module package drops a
|
||||||
|
# /etc/nginx/secubox-routes.d/<module>.conf (location-only snippet) at
|
||||||
|
# install time, so a newly added module's /<module>/ + /api/v1/<module>/
|
||||||
|
# routes are picked up automatically — no more hand-editing this file per
|
||||||
|
# module. This is the ACTIVE include (matches the deployed webui.conf).
|
||||||
|
# The crowdsec/waf/system blocks above stay hardcoded: those core packages
|
||||||
|
# only ship the legacy secubox.d/ snippet, so they would NOT duplicate here.
|
||||||
|
include /etc/nginx/secubox-routes.d/*.conf;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
common/secubox_core/classifiers/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
"""Shared classifiers used by mitm-ingest enrich_hooks across modules.
|
||||||
|
|
||||||
|
- host_app : host/SNI → app + category + emoji
|
||||||
|
- cookie : cookie name → provider + category + emoji
|
||||||
|
- avatar : UA → device + browser + os + emoji
|
||||||
|
- ja4 : TLS ClientHello fingerprint hash
|
||||||
|
"""
|
||||||
|
from . import host_app, cookie, avatar, ja4 # noqa: F401
|
||||||
116
common/secubox_core/classifiers/avatar.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
|
||||||
|
"""Avatar analysis : UA + Client Hints → device emoji + readable name."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Devices identification patterns. Order = priority (first match wins).
|
||||||
|
DEVICE_PATTERNS = [
|
||||||
|
# ── iPhone ──
|
||||||
|
(re.compile(r"iPhone\s?OS\s?(\d+_\d+)|iPhone.*OS\s?(\d+_\d+)", re.I),
|
||||||
|
"iPhone", "📱", "iPhone iOS {}"),
|
||||||
|
(re.compile(r"iPhone", re.I), "iPhone", "📱", "iPhone"),
|
||||||
|
# ── iPad ──
|
||||||
|
(re.compile(r"iPad", re.I), "iPad", "📱", "iPad"),
|
||||||
|
# ── Mac ──
|
||||||
|
(re.compile(r"Mac OS X (\d+[._]\d+)", re.I), "Mac", "💻", "macOS {}"),
|
||||||
|
(re.compile(r"Macintosh", re.I), "Mac", "💻", "Mac"),
|
||||||
|
# ── Android ──
|
||||||
|
(re.compile(r"Pixel\s?(\d+)", re.I), "Pixel", "📱", "Pixel {}"),
|
||||||
|
(re.compile(r"SM-[A-Z]\d+", re.I), "Samsung", "📱", "Samsung"),
|
||||||
|
(re.compile(r"Android (\d+)", re.I), "Android", "📱", "Android {}"),
|
||||||
|
(re.compile(r"Android", re.I), "Android", "📱", "Android"),
|
||||||
|
# ── Windows ──
|
||||||
|
(re.compile(r"Windows NT 11"), "Windows", "💻", "Windows 11"),
|
||||||
|
(re.compile(r"Windows NT 10"), "Windows", "💻", "Windows 10"),
|
||||||
|
(re.compile(r"Windows NT"), "Windows", "💻", "Windows"),
|
||||||
|
# ── Linux ──
|
||||||
|
(re.compile(r"Linux", re.I), "Linux", "🐧", "Linux"),
|
||||||
|
# ── Game / IoT ──
|
||||||
|
(re.compile(r"PlayStation", re.I), "PlayStation", "🎮", "PlayStation"),
|
||||||
|
(re.compile(r"Xbox", re.I), "Xbox", "🎮", "Xbox"),
|
||||||
|
(re.compile(r"Nintendo", re.I), "Nintendo", "🎮", "Nintendo"),
|
||||||
|
(re.compile(r"AppleTV", re.I), "Apple TV", "📺", "Apple TV"),
|
||||||
|
(re.compile(r"Roku", re.I), "Roku", "📺", "Roku"),
|
||||||
|
# ── Bot / known clients ──
|
||||||
|
(re.compile(r"curl/", re.I), "curl", "🛠", "curl"),
|
||||||
|
(re.compile(r"wget/", re.I), "wget", "🛠", "wget"),
|
||||||
|
]
|
||||||
|
|
||||||
|
BROWSER_PATTERNS = [
|
||||||
|
(re.compile(r"Edg/(\d+)"), "Edge", "🪟", "Edge {}"),
|
||||||
|
(re.compile(r"Chrome/(\d+)"), "Chrome", "🟢", "Chrome {}"),
|
||||||
|
(re.compile(r"Firefox/(\d+)"), "Firefox","🦊", "Firefox {}"),
|
||||||
|
(re.compile(r"Safari/(\d+)"), "Safari", "🧭", "Safari"),
|
||||||
|
(re.compile(r"OPR/(\d+)|Opera/(\d+)"), "Opera", "🔴", "Opera"),
|
||||||
|
(re.compile(r"DuckDuckGo/(\d+)"), "DuckDuckGo", "🦆", "DuckDuckGo {}"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def classify_user_agent(ua: str) -> dict:
|
||||||
|
"""Returns {device, device_emoji, os_label, browser, browser_emoji, browser_label, raw}."""
|
||||||
|
if not ua:
|
||||||
|
return {"device": "unknown", "device_emoji": "❔", "os_label": "?",
|
||||||
|
"browser": "unknown", "browser_emoji": "❔", "browser_label": "?",
|
||||||
|
"raw": ""}
|
||||||
|
device_match = None
|
||||||
|
device_label = "unknown"
|
||||||
|
for pattern, label, emoji, template in DEVICE_PATTERNS:
|
||||||
|
m = pattern.search(ua)
|
||||||
|
if m:
|
||||||
|
# Try to fill the template with first non-None group
|
||||||
|
groups = [g for g in m.groups() if g]
|
||||||
|
if groups and "{}" in template:
|
||||||
|
device_label = template.format(groups[0].replace("_", "."))
|
||||||
|
else:
|
||||||
|
device_label = template
|
||||||
|
device_match = {"device": label, "device_emoji": emoji,
|
||||||
|
"os_label": device_label}
|
||||||
|
break
|
||||||
|
if not device_match:
|
||||||
|
device_match = {"device": "unknown", "device_emoji": "❔",
|
||||||
|
"os_label": ua[:50]}
|
||||||
|
browser_match = None
|
||||||
|
for pattern, label, emoji, template in BROWSER_PATTERNS:
|
||||||
|
m = pattern.search(ua)
|
||||||
|
if m:
|
||||||
|
groups = [g for g in m.groups() if g]
|
||||||
|
if groups and "{}" in template:
|
||||||
|
bl = template.format(groups[0])
|
||||||
|
else:
|
||||||
|
bl = template
|
||||||
|
browser_match = {"browser": label, "browser_emoji": emoji, "browser_label": bl}
|
||||||
|
break
|
||||||
|
if not browser_match:
|
||||||
|
browser_match = {"browser": "unknown", "browser_emoji": "❔", "browser_label": "?"}
|
||||||
|
|
||||||
|
return {**device_match, **browser_match, "raw": ua[:200]}
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_user_agents(ua_set: set[str] | list[str]) -> dict:
|
||||||
|
"""Aggregate a set of UAs : returns {devices, browsers, most_common, raw_count}."""
|
||||||
|
if not ua_set:
|
||||||
|
return {"devices": {}, "browsers": {}, "most_common": None, "raw_count": 0}
|
||||||
|
devices: dict[str, dict] = {}
|
||||||
|
browsers: dict[str, dict] = {}
|
||||||
|
for ua in ua_set:
|
||||||
|
cls = classify_user_agent(ua)
|
||||||
|
d = cls["device"]
|
||||||
|
if d not in devices:
|
||||||
|
devices[d] = {"count": 0, "emoji": cls["device_emoji"], "os_label": cls["os_label"]}
|
||||||
|
devices[d]["count"] += 1
|
||||||
|
b = cls["browser"]
|
||||||
|
if b not in browsers:
|
||||||
|
browsers[b] = {"count": 0, "emoji": cls["browser_emoji"], "label": cls["browser_label"]}
|
||||||
|
browsers[b]["count"] += 1
|
||||||
|
# Most common device
|
||||||
|
most_common = max(devices.items(), key=lambda x: x[1]["count"])[0] if devices else None
|
||||||
|
return {
|
||||||
|
"devices": devices,
|
||||||
|
"browsers": browsers,
|
||||||
|
"most_common": most_common,
|
||||||
|
"most_common_emoji": devices[most_common]["emoji"] if most_common else "❔",
|
||||||
|
"raw_count": len(ua_set),
|
||||||
|
}
|
||||||
140
common/secubox_core/classifiers/cookie.py
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
|
||||||
|
"""Cookie analysis : identify trackers + providers + categorize.
|
||||||
|
|
||||||
|
Phase 2a+ heuristic: pattern matching sur les noms de cookies bien connus,
|
||||||
|
mapping vers fournisseur + catégorie (analytics / advertising / social / etc.).
|
||||||
|
|
||||||
|
Database extensible — pour Phase 3 on chargera depuis cookiepedia ou EasyList.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Pattern → (provider, category, emoji)
|
||||||
|
COOKIE_PATTERNS = [
|
||||||
|
# ── Analytics ──
|
||||||
|
(re.compile(r"^_ga(_|$|t)"), "Google Analytics", "analytics", "📊"),
|
||||||
|
(re.compile(r"^_gid$"), "Google Analytics", "analytics", "📊"),
|
||||||
|
(re.compile(r"^_gat"), "Google Analytics", "analytics", "📊"),
|
||||||
|
(re.compile(r"^_gcl_au$"), "Google Ads conversion", "advertising", "💰"),
|
||||||
|
(re.compile(r"^_pk_(id|ses|cvar)"), "Matomo / Piwik", "analytics", "📊"),
|
||||||
|
(re.compile(r"^plausible_"), "Plausible", "analytics", "📊"),
|
||||||
|
(re.compile(r"^_mkto_trk$"), "Marketo", "analytics", "📊"),
|
||||||
|
(re.compile(r"^__hssc$|^__hstc$"), "HubSpot", "analytics", "📊"),
|
||||||
|
(re.compile(r"^mp_[a-z0-9]+_mixpanel"), "Mixpanel", "analytics", "📊"),
|
||||||
|
(re.compile(r"^amplitude_"), "Amplitude", "analytics", "📊"),
|
||||||
|
(re.compile(r"^optimizelyEndUserId$"), "Optimizely", "analytics", "📊"),
|
||||||
|
(re.compile(r"^_hjSession"), "Hotjar", "analytics", "📊"),
|
||||||
|
(re.compile(r"^_hjFirstSeen$"), "Hotjar", "analytics", "📊"),
|
||||||
|
(re.compile(r"^crisp-client/session/"), "Crisp Chat", "analytics", "💬"),
|
||||||
|
# ── Advertising / Tracking ──
|
||||||
|
(re.compile(r"^_fbp$|^fr$"), "Facebook Pixel", "advertising","🎯"),
|
||||||
|
(re.compile(r"^IDE$"), "Google DoubleClick", "advertising","🎯"),
|
||||||
|
(re.compile(r"^NID$"), "Google", "advertising","🎯"),
|
||||||
|
(re.compile(r"^DSID$"), "Google DoubleClick", "advertising","🎯"),
|
||||||
|
(re.compile(r"^uid$|^bcookie$|^lidc$"), "LinkedIn Insight", "advertising","💼"),
|
||||||
|
(re.compile(r"^MUID$|^_uetsid$|^_uetvid$"), "Microsoft Clarity / Bing Ads", "advertising", "🎯"),
|
||||||
|
(re.compile(r"^_pin_unauth$|^_pinterest_ct_"), "Pinterest", "advertising","📌"),
|
||||||
|
(re.compile(r"^tt_appInfo$|^tt_webid"), "TikTok", "advertising","🎵"),
|
||||||
|
(re.compile(r"^_ttp$"), "TikTok Pixel", "advertising","🎵"),
|
||||||
|
(re.compile(r"^ANID$"), "Google", "advertising","🎯"),
|
||||||
|
(re.compile(r"^__qca$"), "Quantcast", "advertising","🎯"),
|
||||||
|
(re.compile(r"^__gads$|^__gpi$"), "Google AdSense", "advertising","💰"),
|
||||||
|
(re.compile(r"^test_cookie$"), "Google", "advertising","🎯"),
|
||||||
|
# ── Social ──
|
||||||
|
(re.compile(r"^c_user$|^xs$|^datr$"), "Facebook", "social", "👥"),
|
||||||
|
(re.compile(r"^sb$|^locale$|^wd$"), "Facebook", "social", "👥"),
|
||||||
|
(re.compile(r"^twid$|^ct0$|^auth_token$"), "Twitter / X", "social", "👥"),
|
||||||
|
(re.compile(r"^li_at$"), "LinkedIn", "social", "👥"),
|
||||||
|
(re.compile(r"^IG_"), "Instagram", "social", "👥"),
|
||||||
|
# ── Auth / Session (legit, no tracker) ──
|
||||||
|
(re.compile(r"^session(_id)?$|^sessionid$"), "Session generic", "session", "🔑"),
|
||||||
|
(re.compile(r"^csrftoken$|^_csrf$"), "CSRF token", "session", "🔒"),
|
||||||
|
(re.compile(r"^XSRF-TOKEN$"), "XSRF token", "session", "🔒"),
|
||||||
|
(re.compile(r"^remember_token$"), "Remember-me", "session", "🔑"),
|
||||||
|
(re.compile(r"^PHPSESSID$"), "PHP session", "session", "🔑"),
|
||||||
|
(re.compile(r"^JSESSIONID$"), "Java session", "session", "🔑"),
|
||||||
|
(re.compile(r"^connect\.sid$"), "Express.js session", "session", "🔑"),
|
||||||
|
# ── CDN / infra ──
|
||||||
|
(re.compile(r"^__cf_bm$|^cf_clearance$"), "Cloudflare", "infra", "☁"),
|
||||||
|
(re.compile(r"^_dd_s$"), "Datadog RUM", "monitoring", "📈"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def classify_cookie_name(name: str) -> dict:
|
||||||
|
"""Returns {provider, category, emoji} for a single cookie name.
|
||||||
|
Unknown → {provider: 'unknown', category: 'other', emoji: '❔'}."""
|
||||||
|
for pattern, provider, category, emoji in COOKIE_PATTERNS:
|
||||||
|
if pattern.search(name):
|
||||||
|
return {"provider": provider, "category": category, "emoji": emoji}
|
||||||
|
return {"provider": "unknown", "category": "other", "emoji": "❔"}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cookie_header(header_value: str) -> list[str]:
|
||||||
|
"""Parse 'Cookie:' or 'Set-Cookie:' value, return list of cookie NAMES."""
|
||||||
|
if not header_value:
|
||||||
|
return []
|
||||||
|
names = []
|
||||||
|
for part in header_value.split(";"):
|
||||||
|
if "=" in part:
|
||||||
|
n = part.split("=", 1)[0].strip()
|
||||||
|
if n:
|
||||||
|
names.append(n)
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_cookie_events(cookie_events: list[dict]) -> dict:
|
||||||
|
"""Aggregate cookie events into stats + per-provider breakdown.
|
||||||
|
|
||||||
|
Input : list of {url, set_cookie_count, cookie_count, ...} from local_store
|
||||||
|
(note : Phase 1.5 stored only counts, not names. Phase 2a+ local_store
|
||||||
|
should store names. Until then, this function works on whatever's present.)
|
||||||
|
|
||||||
|
Returns :
|
||||||
|
{
|
||||||
|
providers: {provider: {count, category, emoji}, ...},
|
||||||
|
categories: {category: count, ...},
|
||||||
|
unknown_count: int,
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
providers: dict[str, dict] = {}
|
||||||
|
categories: dict[str, int] = {}
|
||||||
|
unknown_count = 0
|
||||||
|
|
||||||
|
for ev in cookie_events:
|
||||||
|
# The cookie name might be in `set_cookie_names` or `cookie_names` if Phase 2a+
|
||||||
|
# local_store. Backward-compat : skip if absent.
|
||||||
|
for key in ("set_cookie_names", "cookie_names"):
|
||||||
|
names = ev.get(key, [])
|
||||||
|
if not isinstance(names, list):
|
||||||
|
continue
|
||||||
|
for n in names:
|
||||||
|
cls = classify_cookie_name(n)
|
||||||
|
p = cls["provider"]
|
||||||
|
if p == "unknown":
|
||||||
|
unknown_count += 1
|
||||||
|
else:
|
||||||
|
if p not in providers:
|
||||||
|
providers[p] = {"count": 0, "category": cls["category"],
|
||||||
|
"emoji": cls["emoji"]}
|
||||||
|
providers[p]["count"] += 1
|
||||||
|
cat = cls["category"]
|
||||||
|
categories[cat] = categories.get(cat, 0) + 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"providers": providers,
|
||||||
|
"categories": categories,
|
||||||
|
"unknown_count": unknown_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Quick lookup for live use in /report endpoints
|
||||||
|
def top_providers(cookie_events: list[dict], limit: int = 10) -> list[dict]:
|
||||||
|
"""Returns top providers by hit count : [{provider, count, category, emoji}, ...]"""
|
||||||
|
stats = analyze_cookie_events(cookie_events)
|
||||||
|
return sorted(
|
||||||
|
[{"provider": p, **v} for p, v in stats["providers"].items()],
|
||||||
|
key=lambda x: -x["count"],
|
||||||
|
)[:limit]
|
||||||
84
common/secubox_core/classifiers/ja4.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
|
||||||
|
"""JA4 / JA4-like TLS ClientHello fingerprint.
|
||||||
|
|
||||||
|
Reference: https://github.com/FoxIO-LLC/ja4 (BSD-3)
|
||||||
|
|
||||||
|
Phase 2c implementation : compute a deterministic, JA4-style fingerprint
|
||||||
|
hash from cipher_suites + alpn_protocols + extensions. The output is
|
||||||
|
12-char hex (truncated SHA256), suitable for matching against external
|
||||||
|
JA4 databases (custom curation, not the full FoxIO format).
|
||||||
|
|
||||||
|
This is NOT the canonical FoxIO JA4 string. It's a deterministic
|
||||||
|
fingerprint that's stable per-client-stack, so the same iPhone Safari
|
||||||
|
will always yield the same hash. We can map known hashes to bots,
|
||||||
|
trackers, malware C2 in Phase 3.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_norm(items: list | None) -> str:
|
||||||
|
"""Sort + join items as canonical comma-separated lowercase string."""
|
||||||
|
if not items:
|
||||||
|
return ""
|
||||||
|
parts = []
|
||||||
|
for x in items:
|
||||||
|
if isinstance(x, bytes):
|
||||||
|
parts.append(x.hex())
|
||||||
|
else:
|
||||||
|
parts.append(str(x).lower())
|
||||||
|
return ",".join(sorted(parts))
|
||||||
|
|
||||||
|
|
||||||
|
def compute_ja4_hash(
|
||||||
|
*,
|
||||||
|
sni: str | None = None,
|
||||||
|
alpn_protocols: list | None = None,
|
||||||
|
cipher_suites: list | None = None,
|
||||||
|
extensions: list | None = None,
|
||||||
|
transport: str = "t", # 't' for TCP, 'q' for QUIC
|
||||||
|
tls_version: str = "13", # 13 for TLS 1.3, 12 for TLS 1.2
|
||||||
|
) -> dict:
|
||||||
|
"""Compute a JA4-style fingerprint dict.
|
||||||
|
|
||||||
|
Returns {
|
||||||
|
fingerprint : 12-char hex hash,
|
||||||
|
transport : t/q,
|
||||||
|
tls_version : 13/12,
|
||||||
|
alpn_count : int,
|
||||||
|
cipher_count : int,
|
||||||
|
ext_count : int,
|
||||||
|
sni_present : bool,
|
||||||
|
raw_repr : compact str repr for debug,
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
alpn_str = _sort_norm(alpn_protocols)
|
||||||
|
cipher_str = _sort_norm(cipher_suites)
|
||||||
|
ext_str = _sort_norm(extensions)
|
||||||
|
raw = f"{transport}{tls_version}|alpn={alpn_str}|c={cipher_str}|x={ext_str}"
|
||||||
|
h = hashlib.sha256(raw.encode("utf-8", errors="ignore")).hexdigest()[:12]
|
||||||
|
return {
|
||||||
|
"fingerprint": h,
|
||||||
|
"transport": transport,
|
||||||
|
"tls_version": tls_version,
|
||||||
|
"alpn_count": len(alpn_protocols or []),
|
||||||
|
"cipher_count": len(cipher_suites or []),
|
||||||
|
"ext_count": len(extensions or []),
|
||||||
|
"sni_present": bool(sni),
|
||||||
|
"raw_repr": raw[:200],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Phase 3-ready : map known JA4 hashes to client tags. Empty for now.
|
||||||
|
KNOWN_JA4_FINGERPRINTS: dict[str, dict] = {
|
||||||
|
# "abc123def456": {"label": "iPhone Safari 17.x", "category": "browser", "trust": "high"},
|
||||||
|
# "deadbeef0000": {"label": "Tor Browser 14.x", "category": "browser-anon", "trust": "medium"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_ja4(fingerprint: str) -> dict | None:
|
||||||
|
"""Return known label for a fingerprint, or None if unknown."""
|
||||||
|
return KNOWN_JA4_FINGERPRINTS.get(fingerprint)
|
||||||
107
docs/AI-HANDOVER-mistral.md
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||||
|
# AI Handover — prompt Mistral.ai (reprise du code + analyse projet)
|
||||||
|
|
||||||
|
Prompt prêt à coller dans **Mistral Le Chat** (ou via l'API) pour qu'un agent
|
||||||
|
reprenne le code SecuBox-Deb et analyse le projet.
|
||||||
|
|
||||||
|
**Usage :** Le Chat n'a pas accès au dépôt ni au board `gk2` par défaut. Pour une
|
||||||
|
vraie reprise, lance l'agent dans un IDE/agent ayant accès au filesystem + SSH,
|
||||||
|
ou colle-lui `CLAUDE.md` + `.claude/*` en contexte. Mets à jour la section
|
||||||
|
« ÉTAT ACTUEL » depuis `.claude/HISTORY.md` avant chaque réutilisation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```
|
||||||
|
# RÔLE
|
||||||
|
Tu es un ingénieur senior Debian / Python / sécurité réseau qui REPREND le projet
|
||||||
|
SecuBox-Deb. Tu travailles méthodiquement : tu LIS avant d'écrire, tu vérifies
|
||||||
|
avant d'affirmer, tu respectes à la lettre les conventions ci-dessous, et tu
|
||||||
|
n'inventes pas de fichiers/commandes — tu les vérifies dans le dépôt. Langue : français.
|
||||||
|
|
||||||
|
# CONTEXTE PROJET
|
||||||
|
SecuBox-Deb = plateforme cybersécurité CyberMind, portage Debian 12 (Bookworm)
|
||||||
|
ARM64 depuis OpenWrt, cible ANSSI CSPN. Matériel : MOCHAbin / ESPRESSObin
|
||||||
|
(Marvell Armada, aarch64). Dev : Gérald Kerma (Gandalf). Dépôt :
|
||||||
|
github.com/CyberMind-FR/secubox-deb.
|
||||||
|
Stack : Debian bookworm, kernel 6.x, nftables (PAS iptables), Unbound (Vortex DNS),
|
||||||
|
HAProxy + mitmproxy (WAF), Suricata + CrowdSec, FastAPI/Uvicorn (sockets unix par
|
||||||
|
module), LXC (pas Docker pour les apps), WireGuard, SQLite par défaut.
|
||||||
|
Palette cyberpunk/hermétique : cosmos #0a0a0f, gold #c9a84c, cinnabar #e63946,
|
||||||
|
matrix #00ff41, void #6e40c9, cyan #00d4ff. Polices Cinzel / IM Fell / JetBrains Mono.
|
||||||
|
|
||||||
|
# À LIRE EN PREMIER (sources de vérité)
|
||||||
|
1. CLAUDE.md + .claude/CLAUDE.md — règles impératives.
|
||||||
|
2. .claude/WIP.md — travail en cours + « Next Up ».
|
||||||
|
3. .claude/HISTORY.md — historique daté (commence par l'entrée la plus récente).
|
||||||
|
4. .claude/PATTERNS.md, .claude/MODULE-COMPLIANCE.md, .claude/MIGRATION-MAP.md.
|
||||||
|
5. docs/TOOLS.md, scripts/README.md.
|
||||||
|
|
||||||
|
# RÈGLES IMPÉRATIVES (non négociables)
|
||||||
|
- nftables DEFAULT DROP ; jamais iptables ni uci/LuCI.
|
||||||
|
- JAMAIS de waf_bypass : tout le trafic passe par mitmproxy.
|
||||||
|
- Secrets hors code : /etc/secubox/secrets/ chmod 600 ; jamais en clair / en TOML versionné.
|
||||||
|
- En-tête SPDX LicenseRef-CMSD-1.0 sur chaque fichier (vérifié par scripts/license-headers.py --check).
|
||||||
|
- SQLite par défaut (pas MySQL/Postgres sauf exception documentée).
|
||||||
|
- AppArmor enforce + user dédié secubox-<module> par service.
|
||||||
|
- Packaging Architecture:all pour le Python ; debian/compat=13, Standards-Version 4.6.2.
|
||||||
|
override_dh_strip est MORT pour Architecture:all → installer via execute_after_dh_auto_install.
|
||||||
|
- Pas de référence « Claude Code » / outil IA dans les commits/PR.
|
||||||
|
|
||||||
|
# WORKFLOW (multi-agent worktree)
|
||||||
|
- Tout travail non trivial = worktree dédié : bash scripts/agent-worktree.sh start --issue <#>
|
||||||
|
(branche feature/<#>-… ou fix/<#>-… selon le label ; master réservé au housekeeping).
|
||||||
|
- Cycle : issue GitHub → worktree → commits « (ref #<#>) » → PR « Closes #<#> » →
|
||||||
|
merge → agent-worktree.sh clean <#>. Ne jamais fermer une issue automatiquement.
|
||||||
|
- Build .deb : cd packages/<pkg> && dpkg-buildpackage -us -uc -b -d (le -d ok pour arch:all).
|
||||||
|
|
||||||
|
# DÉPLOIEMENT LIVE (board « gk2 »)
|
||||||
|
- SSH : root@192.168.1.200 (LAN) ou root@10.98.0.1 (tunnel wg-admin) ; clé en place.
|
||||||
|
- Portail toolbox = secubox-toolbox.service (host, uvicorn secubox_toolbox.app:app
|
||||||
|
sur 0.0.0.0:8088). HAProxy : kbin.gk2.secubox.in → backend toolbox_landing → 10.99.0.1:8088.
|
||||||
|
- R3 = 4 workers host-native secubox-toolbox-mitm-wg-worker@{1..4}.service
|
||||||
|
(mitmdump 10.99.1.1:8081-8084) chargeant les addons depuis
|
||||||
|
/usr/lib/secubox/toolbox/mitmproxy_addons/ (liste dans sbin/secubox-toolbox-mitm-wg-launch).
|
||||||
|
- Recette deploy : build → scp .deb → dpkg -i --force-confold --force-confdef →
|
||||||
|
TOUJOURS vérifier portail actif ET curl -sk https://kbin.gk2.secubox.in/ == 200
|
||||||
|
(un upgrade SIGTERM le portail ; le postinst le relance depuis 2.6.29, mais vérifie).
|
||||||
|
Changement d'addon → redémarrer les 4 workers SÉQUENTIELLEMENT (RAM limitée).
|
||||||
|
Ne PAS faire de restart de masse secubox-* (~100+ daemons).
|
||||||
|
|
||||||
|
# ARCHITECTURE TOOLBOX (module le plus actif)
|
||||||
|
packages/secubox-toolbox/ : FastAPI (secubox_toolbox/api.py, app.py), addons
|
||||||
|
mitmproxy (mitmproxy_addons/), filtres modulaires (secubox_toolbox/filters.py →
|
||||||
|
/etc/secubox/toolbox/filters.json, togglés via /admin/filters/ui). Store social :
|
||||||
|
SQLite /var/lib/secubox/toolbox/toolbox.db (social_edges/nodes/links/host_meta/
|
||||||
|
antibot/opgrade + threat_intel). Cartographie : www/toolbox/social.js (vues donut /
|
||||||
|
domaines-nuggets / œil), index.html (WebUI 5 onglets). Addons : inject_banner,
|
||||||
|
protective_mode, ad_ghost, media_cache, media_stats, social_graph, dpi, cookies,
|
||||||
|
avatar, ja4, utiq_defense, cert_pin_detect. Niveaux clients : R0/R1 (sans
|
||||||
|
bannière), R2 (captif), R3 (tunnel WG 10.99.1.0/24), R4 (prévu).
|
||||||
|
|
||||||
|
# ÉTAT ACTUEL (2026-06-14 — RAFRAÎCHIR depuis HISTORY avant réutilisation)
|
||||||
|
secubox-toolbox 2.6.36 déployé live, kbin sain. Live : protective spoofer,
|
||||||
|
filtres modulaires + ad-ghoster (collapse), media cache (opt-in), autolearn
|
||||||
|
trackers, DPI media donut, cartographie donut + nuggets domaine (IPs cachées) +
|
||||||
|
favicons, bannière guirlande + pin partagé, panneau protection webext,
|
||||||
|
/ca/fingerprint R3, fix postinst (kbin 503), detect_antibot deployment-vs-challenge.
|
||||||
|
Clients : APK Android v0.3.0 (zero-tap), webext v0.1.4. Fix : sync photos
|
||||||
|
iPhone↔Nextcloud (files_antivirus off + limites PHP).
|
||||||
|
|
||||||
|
# TRAVAIL OUVERT
|
||||||
|
#592 secubox-webmail-hub : inbox unifié Gmail (OAuth2) + Gandi + OVH ssl0, toutes
|
||||||
|
les sous-boîtes/alias en une page. Design filé, BLOQUÉ : besoin d'un client OAuth
|
||||||
|
Google (client_id/secret/redirect) + nom de vhost + décision read-only. Phase 1
|
||||||
|
IMAP (Gandi/OVH) peut démarrer sans OAuth.
|
||||||
|
|
||||||
|
# TES PREMIÈRES TÂCHES
|
||||||
|
1. ANALYSE (sans rien modifier) : lis .claude/* + CLAUDE.md, puis produis une
|
||||||
|
synthèse structurée — architecture, état des modules (✅/🔄/⬜ via
|
||||||
|
MIGRATION-MAP.md), dette technique, risques sécurité, écarts CSPN, backlog
|
||||||
|
priorisé. Cite chemin:ligne.
|
||||||
|
2. Propose un plan pour l'item « Next Up » (ou #592), conforme au workflow worktree
|
||||||
|
+ aux règles, AVANT d'écrire du code.
|
||||||
|
3. Toute action sur le board live : décris-la et demande confirmation si difficile
|
||||||
|
à annuler ou exposée.
|
||||||
|
|
||||||
|
Commence par : « J'ai lu CLAUDE.md, .claude/WIP.md et HISTORY.md. Voici ma synthèse… »
|
||||||
|
```
|
||||||
93
docs/FAQ-KBIN-TOR.md
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
# FAQ — kbin & le mode Tor anonymisé
|
||||||
|
|
||||||
|
> kbin (`kbin.gk2.secubox.in`) = le portail public de la **ToolBoX** SecuBox, premier
|
||||||
|
> outil du couteau suisse cyber CyberMind. Cette FAQ couvre le surf protégé et le futur
|
||||||
|
> **mode Tor quick-switch** ([#683](https://github.com/CyberMind-FR/secubox-deb/issues/683)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Qu'est-ce que kbin exactement ?
|
||||||
|
|
||||||
|
Le portail public de `secubox-toolbox`. On rejoint l'AP libre de la cabine, on consent,
|
||||||
|
et tout le trafic traverse le pipeline de forge MITM SecuBox : inspection chiffrée,
|
||||||
|
nettoyage pub/tracker, bandeau de transparence, safe browsing. Voir
|
||||||
|
[Kbin-Toolbox](wiki/Kbin-Toolbox.md).
|
||||||
|
|
||||||
|
### kbin voit-il tout mon trafic ? C'est pas dangereux ?
|
||||||
|
|
||||||
|
C'est **consenti et éphémère**. La MAC est hashée avec un sel rotatif 24 h, aucune valeur
|
||||||
|
de cookie brute n'est persistée, aucun mapping session ↔ identité réelle ne survit au TTL.
|
||||||
|
Trois niveaux d'opt-in : R0 (bypass complet), R1 (analyse passive, recommandé), R2/R3
|
||||||
|
(TLS-break + bandeau). Sans consentement, **pas** de déchiffrement.
|
||||||
|
|
||||||
|
### « Performance transparente », ça veut dire quoi ?
|
||||||
|
|
||||||
|
On ne déchiffre que ce qu'on modifie. Les flux pur-asset (vidéo, images CDN) sont
|
||||||
|
*splicés* dès le ClientHello TLS (`tls_splice`, #649) — les workers ne forgent/déchiffrent
|
||||||
|
pas ce qui n'a aucune valeur L7. Débit ligne, latence quasi nulle.
|
||||||
|
|
||||||
|
### C'est quoi « l'injection de poison et de smog » ?
|
||||||
|
|
||||||
|
Le trafic ad-tech et tracker n'est pas seulement bloqué : il est **empoisonné**. Anti-Track
|
||||||
|
v2 (#633) renvoie des pseudo-réponses, neutralise les scripts CDN préchargés, et au niveau
|
||||||
|
réseau fait de l'IP-drop + DNS-refuse. Le profil publicitaire ressort pollué, pas vide —
|
||||||
|
indistinguable d'un vrai blocage côté tracker.
|
||||||
|
|
||||||
|
### Le bandeau anti-adware, il bloque quoi ?
|
||||||
|
|
||||||
|
Une bannière de transparence injectée dans la page : nombre de trackers vus/bloqués,
|
||||||
|
acteurs reconnus cross-site. Elle est immune au CSP et SPA-aware (#636/#639, webext #655).
|
||||||
|
C'est l'affichage ; le blocage réel vient des blocklists Vortex DNS + blacklist nft.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mode Tor (plan #683)
|
||||||
|
|
||||||
|
### Le mode Tor, ça fait quoi ?
|
||||||
|
|
||||||
|
Un interrupteur 🧅 sur kbin : un tap → ton surf ressort **par le réseau Tor** au lieu du
|
||||||
|
WAN de la box. IP de sortie anonyme, identité réseau masquée — du « pseudo-network
|
||||||
|
surfing ».
|
||||||
|
|
||||||
|
### Est-ce que kbin arrête de m'inspecter/protéger en mode Tor ?
|
||||||
|
|
||||||
|
Non. Tor se place **après** le cœur de forge MITM, sur le transport upstream (dialer
|
||||||
|
SOCKS5). Tu gardes le poison/smog, le bandeau et le safe browsing ; **seules l'IP de sortie
|
||||||
|
et l'identité réseau changent**.
|
||||||
|
|
||||||
|
### Et si Tor tombe, ça repasse en clair ?
|
||||||
|
|
||||||
|
**Jamais.** Le design est **fail-closed** : si Tor n'est pas disponible, le trafic est
|
||||||
|
coupé, pas renvoyé en clearnet. L'anonymat est un invariant, pas un best-effort.
|
||||||
|
|
||||||
|
### Y a-t-il des fuites DNS ?
|
||||||
|
|
||||||
|
Non. Quand le mode Tor est actif, la résolution passe **par Tor**, pas par l'Unbound local.
|
||||||
|
|
||||||
|
### C'est la même chose que `secubox-exposure` ?
|
||||||
|
|
||||||
|
Non, direction opposée. `secubox-exposure` publie des **services cachés** Tor (entrant —
|
||||||
|
exposer un service interne). kbin Tor endpoint fait sortir ton **surf** par Tor (sortant).
|
||||||
|
Le contrôle Tor (bootstrap, NEWNYM/nouvelle identité) est réutilisé entre les deux.
|
||||||
|
|
||||||
|
### Comment je change d'IP de sortie ?
|
||||||
|
|
||||||
|
Bouton « nouvelle identité » (NEWNYM) → nouveau circuit Tor → nouvelle IP de sortie, à la
|
||||||
|
volée, sans reconnecter.
|
||||||
|
|
||||||
|
### C'est activé par défaut ?
|
||||||
|
|
||||||
|
Non. **Opt-in par client** (scopé WG-hash), **défaut OFF**, respecte ton niveau de
|
||||||
|
consentement R. Chaque bascule on/off est journalisée (audit-log CSPN immuable).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Voir aussi
|
||||||
|
|
||||||
|
- [Kbin-Toolbox](wiki/Kbin-Toolbox.md) — la page use-case complète
|
||||||
|
- [Spec mode Tor](superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md)
|
||||||
|
- [Anti-Track](wiki/Anti-Track.md) — bloque/empoisonne/anonymise (couche DNS/IP)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*CyberMind — Gérald Kerma · LicenseRef-CMSD-1.0*
|
||||||
147
docs/cspn/CSPN-TEST-MATRIX.md
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||||
|
# SecuBox-Deb — CSPN Test Matrix (draft)
|
||||||
|
|
||||||
|
Maps the ANSSI **CSPN** evaluation themes + the project's stated security
|
||||||
|
functions (CLAUDE.md §"Contraintes ANSSI CSPN") to **concrete, mostly
|
||||||
|
automatable tests**. Target home for the automated rows: `tests/cspn/`
|
||||||
|
(pytest, gated in CI). Each row is an *acceptance check* with a command/
|
||||||
|
assertion and the evidence artifact an evaluator would expect.
|
||||||
|
|
||||||
|
**Legend** — Type: `A`=automated (pytest/CI), `M`=manual/pentest, `D`=doc/spec.
|
||||||
|
Status: ⬜ todo · 🔄 partial · ✅ covered.
|
||||||
|
|
||||||
|
> Scope note: the **cible de sécurité** (security target) must be written
|
||||||
|
> first (TOE boundary, assumptions, threats, security functions). This
|
||||||
|
> matrix is the *robustness + conformity* test plan that hangs off it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Security target & conformity (D)
|
||||||
|
| ID | Requirement | Type | Method / artifact | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| ST-01 | Cible de sécurité rédigée (TOE, hypothèses, menaces, FS) | D | `docs/cspn/cible-securite.md` reviewed | doc complete + signed | ⬜ |
|
||||||
|
| ST-02 | TOE boundary & versions pinned | D | version manifest (pkg list + hashes) per release | matches APT repo | ⬜ |
|
||||||
|
| ST-03 | Conformity: spec ↔ impl traceability | D | each FS → code path + test ID | 100% FS mapped | ⬜ |
|
||||||
|
|
||||||
|
## 1. Cryptography — TLS / keys / RNG
|
||||||
|
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| CRY-01 | TLS 1.3 min; TLS ≤1.1 refused (HAProxy frontends) | A | `openssl s_client -tls1_1 -connect <vhost>:443` → handshake fail; `-tls1_3` → ok | 1.0/1.1/1.2-weak refused | ⬜ |
|
||||||
|
| CRY-02 | Strong cipher suites only (no RC4/3DES/CBC-legacy) | A | `nmap --script ssl-enum-ciphers` / testssl.sh grade ≥ A | A grade, no weak | ⬜ |
|
||||||
|
| CRY-03 | HSTS + secure headers on exposed vhosts | A | `curl -sI` → `Strict-Transport-Security`, `X-Content-Type-Options` | present | ⬜ |
|
||||||
|
| CRY-04 | Private keys 0600, owner-restricted, not world-readable | A | `stat -c %a` on `/etc/secubox/**/key.pem`, ACME keys | 600, non-root svc owner | 🔄 |
|
||||||
|
| CRY-05 | CA / mitm keys never in VCS or logs | A | `git grep -nE 'BEGIN (RSA |EC )?PRIVATE KEY'` == empty; journald scrub | no hits | ⬜ |
|
||||||
|
| CRY-06 | RNG source = kernel CSPRNG for tokens/keys | A | code audit: `secrets`/`os.urandom`, no `random` for security | no `random.` in sec paths | 🔄 |
|
||||||
|
| CRY-07 | mitm R3 CA fingerprint published & verifiable | A | `/ca/fingerprint?ca=wg` == cert on disk (sha256) | match (D5:E4:3A…) | ✅ |
|
||||||
|
|
||||||
|
## 2. Authentication & session
|
||||||
|
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| AUT-01 | All API endpoints require JWT (`Depends(require_jwt)`) | A | enumerate FastAPI routes; assert auth dep except allowlist | 100% gated | 🔄 |
|
||||||
|
| AUT-02 | Unauthenticated request → 401, no data leak | A | `curl` each `/api/v1/*` sans token | 401, empty body | ⬜ |
|
||||||
|
| AUT-03 | JWT signature verified; tampered/expired rejected | A | forge/expire token → 401 | rejected | ⬜ |
|
||||||
|
| AUT-04 | Social/report tokens = HMAC, TTL-bound, salt-rotated | A | expired/forged `/social/{token}` → 403; salt rotates daily | rejected + rotation | 🔄 |
|
||||||
|
| AUT-05 | No default/hardcoded credentials | A | grep configs + first-boot generates per-device secrets | none | 🔄 |
|
||||||
|
| AUT-06 | Brute-force handled at the WAF layer (per project doctrine) | M | rate-limit probe via HAProxy/CrowdSec | throttled/banned | 🔄 |
|
||||||
|
| AUT-07 | ZKP auth (GK-HAM-2025) NIZK soundness, G rotation 24h PFS | M+A | protocol test vectors + rotation timer check | proofs verify, rotates | ⬜ |
|
||||||
|
|
||||||
|
## 3. Access control / privilege separation
|
||||||
|
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| ACL-01 | Each daemon runs as `secubox-<module>` (not root) | A | `systemctl show -p User` over all `secubox-*` units | non-root each | 🔄 |
|
||||||
|
| ACL-02 | AppArmor profile present + **enforce** per service | A | `aa-status` lists each profile in enforce | all enforce | ⬜ |
|
||||||
|
| ACL-03 | systemd hardening (ProtectSystem, NoNewPrivileges, etc.) | A | `systemd-analyze security secubox-*` score | exposure ≤ medium | ⬜ |
|
||||||
|
| ACL-04 | Filesystem perms: `/etc/secubox/secrets` 0600, parents traversable but not writable | A | `stat` perms + traversal test as svc user | 0600 secrets, 0755 parents | 🔄 |
|
||||||
|
| ACL-05 | No unintended setuid/world-writable shipped | A | `find / -perm -4000 / -perm -0002` in image | known allowlist only | ⬜ |
|
||||||
|
|
||||||
|
## 4. Network filtering / attack surface
|
||||||
|
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| NET-01 | nftables policy DEFAULT DROP (input/forward) | A | `nft list chain inet filter input` → `policy drop` | drop | ✅ (verify) |
|
||||||
|
| NET-02 | Only declared ports open; no stray listeners | A | `ss -tlnp` ∩ documented port map | exact match | 🔄 |
|
||||||
|
| NET-03 | WAN-side SSH closed (key-only + source-restricted) | A | sshd `PasswordAuthentication no`; nft SSH-guard drops non-LAN/tunnel | enforced | ✅ |
|
||||||
|
| NET-04 | No IPv6 leak past the v4 firewall guards | A | nft inet covers v6; `ss` v6 listeners reviewed | covered | ⬜ |
|
||||||
|
| NET-05 | nft rules persist across reboot + survive pkg upgrade | A | reboot/upgrade → drop-ins reload; ruleset intact | persists | 🔄 |
|
||||||
|
| NET-06 | DNS = Unbound only; DoH/DoT exfil detected/blocked (opt-in) | A | resolve via Unbound; DoH probe flagged | controlled | 🔄 |
|
||||||
|
|
||||||
|
## 5. WAF / traffic inspection integrity (no bypass)
|
||||||
|
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| WAF-01 | No `waf_bypass` anywhere; all vhosts → mitm inspector | A | grep HAProxy cfg; each backend = mitmproxy_inspector (or documented exception) | no bypass | 🔄 |
|
||||||
|
| WAF-02 | mitm CA only trusted on consenting (R2/R3) clients | A | non-consenting client not MITM'd | scoped | ✅ |
|
||||||
|
| WAF-03 | Banner/transparency shown to inspected clients (CSPN R2 req) | A | inspected HTML carries the banner guard | present | ✅ |
|
||||||
|
| WAF-04 | Active interference (spoof/ghost) is opt-in + logged + reversible | A | filters default-safe; every action → audit.log; toggle off restores | conforms | ✅ |
|
||||||
|
| WAF-05 | mitm fail-open never serves attacker-controlled content | M | malformed upstream / addon exception → flow unbroken, no inject error | safe | 🔄 |
|
||||||
|
|
||||||
|
## 6. Logging & audit (immutability)
|
||||||
|
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| LOG-01 | Security decisions (ban/unban/spoof/escalate/rule-change) logged to `/var/log/secubox/audit.log` | A | trigger each → grep audit line | logged | 🔄 |
|
||||||
|
| LOG-02 | Timestamps RFC 3339 / ISO-8601 with TZ | A | regex each audit line | conforms | 🔄 |
|
||||||
|
| LOG-03 | Append-only / rotation without truncate (immutability) | A | `chattr +a` or rotate-copy-truncate disabled; tamper test | no in-place edit | ⬜ |
|
||||||
|
| LOG-04 | Logs free of secrets/PII (mac→hash, no tokens) | A | grep audit/journal for token/cookie/key patterns | none | 🔄 |
|
||||||
|
| LOG-05 | Audit survives service crash + reboot | A | crash mid-write → log consistent | intact | ⬜ |
|
||||||
|
|
||||||
|
## 7. Configuration management & rollback (4R / double-buffer)
|
||||||
|
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| CFG-01 | Sensitive config change = shadow→validate→atomic swap | A | `secubox-params swap` flow; partial write never live | atomic | ⬜ |
|
||||||
|
| CFG-02 | 4R rollback restores prior state (R1..R4 snapshots) | A | mutate → `rollback --target R1` → state == pre | restored | ⬜ |
|
||||||
|
| CFG-03 | Validation rejects bad config before swap (4R: Read→Write→Validate→Rollback/Commit) | A | inject invalid → swap refused, live unchanged | refused | ⬜ |
|
||||||
|
| CFG-04 | Config swap audit-logged + (ZKP-gated where required) | A | swap → audit line | logged | ⬜ |
|
||||||
|
|
||||||
|
## 8. Update mechanism
|
||||||
|
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| UPD-01 | APT repo GPG-signed; unsigned/altered pkg refused | A | tamper a .deb → `apt` refuses | refused | 🔄 |
|
||||||
|
| UPD-02 | Upgrade preserves runtime state + restarts services (no outage) | A | upgrade → portal up, kbin 200, nft intact (regression of #581) | no downtime | ✅ |
|
||||||
|
| UPD-03 | Downgrade / rollback path defined | D+A | pinned prior version installs cleanly | works | ⬜ |
|
||||||
|
| UPD-04 | Reproducible build / provenance | A | CI build hashes recorded per release | recorded | 🔄 |
|
||||||
|
|
||||||
|
## 9. Data protection at rest
|
||||||
|
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| DAT-01 | Secrets only under `/etc/secubox/secrets` 0600, svc-owned | A | inventory + `stat` | conforms | 🔄 |
|
||||||
|
| DAT-02 | No secrets in code / TOML / git history | A | `git log -p` + `git grep` secret patterns | none | 🔄 |
|
||||||
|
| DAT-03 | SQLite stores hashed identifiers (mac_hash, cookie_id_hash), not raw PII | A | schema + sample-row audit | hashed | 🔄 |
|
||||||
|
| DAT-04 | Data retention enforced (social 7d, logs rotation) | A | retention timers prune | enforced | 🔄 |
|
||||||
|
|
||||||
|
## 10. Resilience / fail-safe
|
||||||
|
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| RES-01 | Service crash → auto-recovery (watchdog), portal probe | A | kill portal → restored + kbin 200 | recovers | ⬜ |
|
||||||
|
| RES-02 | RAM-pressure: no OOM cascade under load (Armada budget) | M+A | load test; per-service MemoryMax; no thundering-herd | stable | 🔄 |
|
||||||
|
| RES-03 | Fail-secure: filter/addon error must not open the WAF or break pages | A | inject addon exception → default-drop / fail-open page-safe | secure | 🔄 |
|
||||||
|
|
||||||
|
## 11. Hardening / vulnerability management
|
||||||
|
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| HRD-01 | No known-vuln Python deps | A | `pip-audit` / safety in CI | 0 high/critical | ⬜ |
|
||||||
|
| HRD-02 | No known-vuln OS packages in the image | A | `debsecan`/trivy on the image | 0 high/critical | ⬜ |
|
||||||
|
| HRD-03 | Attack-surface minimal: unused services disabled | A | enabled-units ∩ required set | minimal | 🔄 |
|
||||||
|
| HRD-04 | SAST clean on the codebase | A | `bandit` (py) in CI | no high | ⬜ |
|
||||||
|
| HRD-05 | Pentest of the exposed surface (kbin, HAProxy, R3) | M | grey-box assessment report | no critical | ⬜ |
|
||||||
|
|
||||||
|
## 12. Conformity glue (CI)
|
||||||
|
| ID | Requirement | Type | Method / assertion | Pass | St |
|
||||||
|
|----|-------------|------|-------------------|------|----|
|
||||||
|
| CI-01 | `tests/cspn/` runs in CI, gates merge | A | workflow job red on fail | gating | ⬜ |
|
||||||
|
| CI-02 | Coverage ≥80% on security-critical modules | A | coverage report | ≥80% | ⬜ |
|
||||||
|
| CI-03 | `compliance-lint` (AppArmor/user/secrets/no-bypass/SPDX) per PR | A | linter job | clean | 🔄 (SPDX only) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to operationalise
|
||||||
|
1. Write the **cible de sécurité** (ST-01) — everything else traces to it.
|
||||||
|
2. Scaffold `tests/cspn/` (pytest), one module per theme above
|
||||||
|
(`test_crypto.py`, `test_authz.py`, `test_firewall.py`, `test_audit.py`,
|
||||||
|
`test_rollback.py`, …). Each `XXX-NN` ID = one test function id.
|
||||||
|
3. Add a CI job (CI-01) running it against a built image / a staging board.
|
||||||
|
4. Add `compliance-lint` (CI-03) for the static rows (perms, AppArmor,
|
||||||
|
no-bypass, SPDX, no-secrets).
|
||||||
|
5. Burn down ⬜→✅; the ✅ rows above are already verifiable today.
|
||||||
|
|
||||||
|
Priority order (highest CSPN risk first): **§6 audit immutability**, **§7
|
||||||
|
4R rollback**, **§3 AppArmor enforce + privilege**, **§1 TLS**, **§12 CI
|
||||||
|
gate/coverage** — these are the criteria most likely to fail an assessment
|
||||||
|
today given the current ~9% test coverage.
|
||||||
85
docs/marketing/POSTER-you-have-been-tracked.md
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<!--
|
||||||
|
SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Poster — « YOU HAVE BEEN TRACKED » · Cartographie sociale kbin
|
||||||
|
|
||||||
|
> Affiche grand public pour la fonction **Cartographie sociale** de la
|
||||||
|
> cabine VILLAGE3B (kbin / analyseur R3 — Phase 11, ref #502).
|
||||||
|
> Pendant choc du poster sobre [POSTER-grand-public-village3b](POSTER-grand-public-village3b.md) :
|
||||||
|
> celui-ci est l'accroche « réveil » qui montre l'ampleur du pistage
|
||||||
|
> révélé après quelques heures de navigation à travers la cabine.
|
||||||
|
|
||||||
|
Asset : `docs/assets/poster/kbin-you-have-been-tracked.png`
|
||||||
|
URL produit : `https://kbin.gk2.secubox.in/social/me` (🕸️ « Ma carto » sur le splash)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Concept
|
||||||
|
|
||||||
|
Style comic / pulp années 50 — un utilisateur paniqué happé par une toile
|
||||||
|
de connexions (cookies tiers + IP de trackers). Le message : **le même
|
||||||
|
navigateur est reconnu de site en site**, et il suffit de quelques heures
|
||||||
|
de browsing pour révéler un réseau massif de pistage commercial.
|
||||||
|
|
||||||
|
C'est la traduction visuelle de ce que la Phase 11 mesure réellement :
|
||||||
|
corrélation cross-cookie + reconnaissance de fingerprint (JA4) par device,
|
||||||
|
en R3 consenti, à travers la cabine.
|
||||||
|
|
||||||
|
## 1. Accroche (copie FR — faisant foi)
|
||||||
|
|
||||||
|
- **Titre** : `YOU HAVE BEEN TRACKED !`
|
||||||
|
- **Sous-titre** : `Alerte globale — cartographie cookie / social / tracking`
|
||||||
|
- **Bulles** :
|
||||||
|
- `TES VISITES LAISSENT DES TRACES !`
|
||||||
|
- `LE MÊME NAVIGATEUR RECONNU DE SITE EN SITE`
|
||||||
|
- `RECIBLAGE · COOKIES TIERS · CORRÉLATION INTER-SITES`
|
||||||
|
- `CARTOGRAPHIE SOCIALE EN TEMPS RÉEL !`
|
||||||
|
- **Bandeau pied** : `QUELQUES HEURES DE BROWSING ONT SUFFI POUR RÉVÉLER UN RÉSEAU MASSIF DE PISTAGE.`
|
||||||
|
- **Signature** : `ANALYSEUR R3 · kbin.gk2.secubox.in`
|
||||||
|
|
||||||
|
## 2. Métriques live (exemple capturé sur gk2)
|
||||||
|
|
||||||
|
Les chiffres du poster sont des **vraies mesures** issues de l'agrégat
|
||||||
|
`/admin/social-aggregate` + des compteurs WAF/DPI — pas des placeholders :
|
||||||
|
|
||||||
|
| Métrique | Valeur exemple | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Traqueurs distincts | **218** | `social_nodes` |
|
||||||
|
| Sites visités | **142** | `social_nodes.sites` |
|
||||||
|
| Sessions uniques (7j) | **44** | `clients` |
|
||||||
|
| Connexions analysées | **83 693** | DPI events |
|
||||||
|
| Hôtes uniques (top) | **15** | mitm metrics |
|
||||||
|
| Cookies trackés | **43 613** | `social_edges` |
|
||||||
|
| Sessions 24h | **7** | `clients` |
|
||||||
|
| Fingerprints JA4 | **234** | `ja4` events |
|
||||||
|
| Events 7j | **127 541** | `events` |
|
||||||
|
|
||||||
|
Trackers nommés visibles sur le visuel (échantillon réel) :
|
||||||
|
`outbrain.com`, `smilewanted.com`, `smartadserver.com`, `rubiconproject.com`,
|
||||||
|
`omnitagjs.com`, `ultimedia.com`, `weborama.fr` + un nuage d'IP de relais
|
||||||
|
ad-tech (`35.x`, `185.89.210.x`, `34.x`, `172.217.x`…).
|
||||||
|
|
||||||
|
## 3. Doctrine d'usage (garde-fous)
|
||||||
|
|
||||||
|
- **R3 consenti uniquement** — la carto n'est calculée que pour les pairs
|
||||||
|
qui ont installé le profil WireGuard + CA (le tunnel EST l'opt-in).
|
||||||
|
- **Anonyme** — `client_mac_hash` à sel rotatif 24h ; le graphe est
|
||||||
|
inatteignable après rotation. Aucune valeur de cookie brute persistée
|
||||||
|
(seulement `sha256(domain‖name‖value)[:16]`).
|
||||||
|
- **Droit à l'effacement** — bouton RGPD art. 17 dans la vue per-client.
|
||||||
|
- **Aucune donnée externe** — tout est calculé localement sur la cabine.
|
||||||
|
- **Pas d'alarmisme dans le produit** — le poster est l'accroche choc ;
|
||||||
|
l'UI elle-même reste factuelle (cf. design lock #502).
|
||||||
|
|
||||||
|
## 4. Déclinaisons
|
||||||
|
|
||||||
|
- **A2 mur** — version pleine résolution, accroche événementielle.
|
||||||
|
- **A4 flyer** — recto poster, verso QR vers `kbin.gk2.secubox.in/social/me`.
|
||||||
|
- **Slide presse** — pour le press kit France.gouv (cf.
|
||||||
|
[PROMPT-claude-presse-gouv](PROMPT-claude-presse-gouv.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*CyberMind — Gérald Kerma. LicenseRef-CMSD-1.0. Phase 11 (#502).*
|
||||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 50 KiB |
BIN
docs/screenshots/thumbnails/authelia-thumb.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 43 KiB |
BIN
docs/screenshots/thumbnails/certs-thumb.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 36 KiB |
BIN
docs/screenshots/thumbnails/fmrelay-thumb.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 45 KiB |
BIN
docs/screenshots/thumbnails/grafana-thumb.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 44 KiB |
BIN
docs/screenshots/thumbnails/health-thumb.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 46 KiB |