diff --git a/dementor/__init__.py b/dementor/__init__.py index cb07375..725a248 100644 --- a/dementor/__init__.py +++ b/dementor/__init__.py @@ -18,5 +18,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -__version__ = "1.0.0.dev24" +__version__ = "1.0.0.dev25" __author__ = "MatrixEditor" diff --git a/dementor/assets/Dementor.toml b/dementor/assets/Dementor.toml index 35f1cca..e2c3dea 100644 --- a/dementor/assets/Dementor.toml +++ b/dementor/assets/Dementor.toml @@ -1201,8 +1201,9 @@ PresentationUri = "/present.html" [Log] # Enable logging output to a file +# Changed in v1.0.0.dev25 - now enabled by default -Enabled = false +Enabled = true # Specify custom log directory under the current workspace diff --git a/dementor/assets/banner.txt b/dementor/assets/banner.txt index dfb32cb..01d0971 100644 --- a/dementor/assets/banner.txt +++ b/dementor/assets/banner.txt @@ -4,17 +4,17 @@ [on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #010202][/on #000000] [on #020506][/on #010202] [on #000000][/on #020506] [on #000000][/on #000000] [on #010202][/on #000000] [on #000000][/on #010202] [on #59665d][/on #000000]-[on #abe6d2][/on #59665d]%[on #b3ebd1][/on #abe6d2]%[on #afe9cd][/on #b3ebd1]%[on #b5eacf][/on #afe9cd]%[on #c1ecd0][/on #b5eacf]%[on #cdf0d3][/on #c1ecd0]%[on #c6efd5][/on #cdf0d3]%[on #abd2c1][/on #c6efd5]#[on #353431][/on #abd2c1] [on #000000][/on #353431] [on #020202][/on #000000] [on #000101][/on #020202] [on #000000][/on #000101] [on #030708][/on #000000] [on #000000][/on #030708] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #040a0a][/on #000000] [on #000102][/on #040a0a] [on #020304][/on #000102] [on #010405][/on #020304] [on #23201e][/on #010405] [on #83d1c9][/on #23201e]#[on #99e1d0][/on #83d1c9]#[on #9de5cc][/on #99e1d0]%[on #a3f1d7][/on #9de5cc]%[on #acf6dd][/on #a3f1d7]%[on #b4f7dd][/on #acf6dd]%[on #bdf4d7][/on #b4f7dd]%[on #cbebce][/on #bdf4d7]%[on #a0e5d3][/on #cbebce]%[on #87bbaf][/on #a0e5d3]*[on #060202][/on #87bbaf] [on #03090a][/on #060202] [on #000101][/on #03090a] [on #010202][/on #000101] [on #050a0a][/on #010202] [on #000000][/on #050a0a] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #010304][/on #000000] [on #071314][/on #010304] [on #071415][/on #071314] [on #091516][/on #071415] [on #0c1416][/on #091516] [on #000000][/on #0c1416] [on #576762][/on #000000]-[on #5eb8b9][/on #576762]*[on #98e2d0][/on #5eb8b9]#[on #82ddca][/on #98e2d0]#[on #699f9b][/on #82ddca]+[on #638083][/on #699f9b]=[on #658988][/on #638083]=[on #80bbb0][/on #658988]*[on #9fedd6][/on #80bbb0]%[on #87d7ca][/on #9fedd6]#[on #71c2be][/on #87d7ca]*[on #343330][/on #71c2be] [on #000202][/on #343330] [on #0c1617][/on #000202] [on #071011][/on #0c1617] [on #081516][/on #071011] [on #050e0e][/on #081516] [on #000000][/on #050e0e] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] -[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000101][/on #000000] [on #020405][/on #000101] [on #020506][/on #020405] [on #04080a][/on #020506] [on #071011][/on #04080a] [on #050b0b][/on #071011] [on #090c10][/on #050b0b] [on #101c22][/on #090c10] [on #06050c][/on #101c22] [on #77a8a2][/on #06050c]*[on #499fa5][/on #77a8a2]+[on #9ad5c5][/on #499fa5]#[on #39525c][/on #9ad5c5] [on #2a2a42][/on #39525c] [on #020321][/on #2a2a42] [on #080925][/on #020321] [on #313449][/on #080925] [on #678587][/on #313449]=[on #87cfc2][/on #678587]#[on #4da4a9][/on #87cfc2]+[on #6d7f78][/on #4da4a9]=[on #000001][/on #6d7f78] [on #121d21][/on #000001] [on #05070b][/on #121d21] [on #071012][/on #05070b] [on #060c0e][/on #071012] [on #020607][/on #060c0e] [on #010304][/on #020607] [on #010304][/on #010304] [on #000001][/on #010304] [on #000000][/on #000001] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [bold]Dementor [white]v{dementor_version}[/white][/bold] -[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000001][/on #000000] [on #010203][/on #000001] [on #020506][/on #010203] [on #091414][/on #020506] [on #081214][/on #091414] [on #0d2629][/on #081214] [on #12292e][/on #0d2629] [on #0e2835][/on #12292e] [on #00162f][/on #0e2835] [on #1e364b][/on #00162f] [on #72b9b4][/on #1e364b]*[on #81c5c1][/on #72b9b4]*[on #475f68][/on #81c5c1]-[on #060826][/on #475f68] [on #262e42][/on #060826] [on #0f162d][/on #262e42] [on #141a31][/on #0f162d] [on #23293f][/on #141a31] [on #050a26][/on #23293f] [on #668d8e][/on #050a26]+[on #8ad8cd][/on #668d8e]#[on #7da5a0][/on #8ad8cd]*[on #08213a][/on #7da5a0] [on #062137][/on #08213a] [on #0f2f39][/on #062137] [on #13292c][/on #0f2f39] [on #0c2122][/on #13292c] [on #071113][/on #0c2122] [on #081212][/on #071113] [on #010304][/on #081212] [on #010203][/on #010304] [on #000000][/on #010203] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [i]Advanced mDNS/LLMNR/NBTNS multicast[/i] -[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000101][/on #000000] [on #010303][/on #000101] [on #010404][/on #010303] [on #091919][/on #010404] [on #070f0f][/on #091919] [on #0c171b][/on #070f0f] [on #11212d][/on #0c171b] [on #102b3e][/on #11212d] [on #2d5668][/on #102b3e] [on #4c7984][/on #2d5668]=[on #8fcbc2][/on #4c7984]#[on #7dd5ca][/on #8fcbc2]#[on #7c9b9e][/on #7dd5ca]+[on #00112b][/on #7c9b9e] [on #0f142c][/on #00112b] [on #071028][/on #0f142c] [on #0d132a][/on #071028] [on #0c1228][/on #0d132a] [on #091128][/on #0c1228] [on #090d25][/on #091128] [on #163047][/on #090d25] [on #a0cdc2][/on #163047]#[on #84d6ca][/on #a0cdc2]#[on #8abbb8][/on #84d6ca]*[on #396c7a][/on #8abbb8]-[on #285162][/on #396c7a] [on #0d2336][/on #285162] [on #12232d][/on #0d2336] [on #081215][/on #12232d] [on #091515][/on #081215] [on #0b1717][/on #091515] [on #010101][/on #0b1717] [on #010304][/on #010101] [on #000000][/on #010304] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [i]poisoner and rogue service provider[/] +[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000101][/on #000000] [on #020405][/on #000101] [on #020506][/on #020405] [on #04080a][/on #020506] [on #071011][/on #04080a] [on #050b0b][/on #071011] [on #090c10][/on #050b0b] [on #101c22][/on #090c10] [on #06050c][/on #101c22] [on #77a8a2][/on #06050c]*[on #499fa5][/on #77a8a2]+[on #9ad5c5][/on #499fa5]#[on #39525c][/on #9ad5c5] [on #2a2a42][/on #39525c] [on #020321][/on #2a2a42] [on #080925][/on #020321] [on #313449][/on #080925] [on #678587][/on #313449]=[on #87cfc2][/on #678587]#[on #4da4a9][/on #87cfc2]+[on #6d7f78][/on #4da4a9]=[on #000001][/on #6d7f78] [on #121d21][/on #000001] [on #05070b][/on #121d21] [on #071012][/on #05070b] [on #060c0e][/on #071012] [on #020607][/on #060c0e] [on #010304][/on #020607] [on #010304][/on #010304] [on #000001][/on #010304] [on #000000][/on #000001] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [white bold]Dementor v{dementor_version}[/white bold] +[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000001][/on #000000] [on #010203][/on #000001] [on #020506][/on #010203] [on #091414][/on #020506] [on #081214][/on #091414] [on #0d2629][/on #081214] [on #12292e][/on #0d2629] [on #0e2835][/on #12292e] [on #00162f][/on #0e2835] [on #1e364b][/on #00162f] [on #72b9b4][/on #1e364b]*[on #81c5c1][/on #72b9b4]*[on #475f68][/on #81c5c1]-[on #060826][/on #475f68] [on #262e42][/on #060826] [on #0f162d][/on #262e42] [on #141a31][/on #0f162d] [on #23293f][/on #141a31] [on #050a26][/on #23293f] [on #668d8e][/on #050a26]+[on #8ad8cd][/on #668d8e]#[on #7da5a0][/on #8ad8cd]*[on #08213a][/on #7da5a0] [on #062137][/on #08213a] [on #0f2f39][/on #062137] [on #13292c][/on #0f2f39] [on #0c2122][/on #13292c] [on #071113][/on #0c2122] [on #081212][/on #071113] [on #010304][/on #081212] [on #010203][/on #010304] [on #000000][/on #010203] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [i white]Advanced mDNS/LLMNR/NBTNS multicast[/i white] +[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000101][/on #000000] [on #010303][/on #000101] [on #010404][/on #010303] [on #091919][/on #010404] [on #070f0f][/on #091919] [on #0c171b][/on #070f0f] [on #11212d][/on #0c171b] [on #102b3e][/on #11212d] [on #2d5668][/on #102b3e] [on #4c7984][/on #2d5668]=[on #8fcbc2][/on #4c7984]#[on #7dd5ca][/on #8fcbc2]#[on #7c9b9e][/on #7dd5ca]+[on #00112b][/on #7c9b9e] [on #0f142c][/on #00112b] [on #071028][/on #0f142c] [on #0d132a][/on #071028] [on #0c1228][/on #0d132a] [on #091128][/on #0c1228] [on #090d25][/on #091128] [on #163047][/on #090d25] [on #a0cdc2][/on #163047]#[on #84d6ca][/on #a0cdc2]#[on #8abbb8][/on #84d6ca]*[on #396c7a][/on #8abbb8]-[on #285162][/on #396c7a] [on #0d2336][/on #285162] [on #12232d][/on #0d2336] [on #081215][/on #12232d] [on #091515][/on #081215] [on #0b1717][/on #091515] [on #010101][/on #0b1717] [on #010304][/on #010101] [on #000000][/on #010304] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [i white]poisoner and rogue service provider[/] [on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #010203][/on #000000] [on #010202][/on #010203] [on #103534][/on #010202] [on #0a1d1e][/on #103534] [on #03000a][/on #0a1d1e] [on #152f3d][/on #03000a] [on #05354e][/on #152f3d] [on #21586e][/on #05354e] [on #b1e0d1][/on #21586e]%[on #94f3d9][/on #b1e0d1]%[on #7bdfcd][/on #94f3d9]#[on #45a9ae][/on #7bdfcd]+[on #60959c][/on #45a9ae]+[on #3b7381][/on #60959c]-[on #0c1930][/on #3b7381] [on #0a102a][/on #0c1930] [on #0c132b][/on #0a102a] [on #0c142c][/on #0c132b] [on #0c0c25][/on #0c142c] [on #10253c][/on #0c0c25] [on #487683][/on #10253c]=[on #52949f][/on #487683]+[on #4dbabb][/on #52949f]*[on #8ae8d3][/on #4dbabb]#[on #b4f7dd][/on #8ae8d3]%[on #9cbeb7][/on #b4f7dd]#[on #04435e][/on #9cbeb7] [on #0b384d][/on #04435e] [on #122836][/on #0b384d] [on #020005][/on #122836] [on #103232][/on #020005] [on #0c2323][/on #103232] [on #010202][/on #0c2323] [on #000101][/on #010202] [on #000000][/on #000101] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000202][/on #000000] [on #000101][/on #000202] [on #010303][/on #000101] [on #08191a][/on #010303] [on #1b575a][/on #08191a] [on #174451][/on #1b575a] [on #0b4b62][/on #174451] [on #56999e][/on #0b4b62]+[on #97dacd][/on #56999e]#[on #9ee3d2][/on #97dacd]%[on #85ddcb][/on #9ee3d2]#[on #8fecd4][/on #85ddcb]%[on #4fa7a9][/on #8fecd4]+[on #2c7585][/on #4fa7a9]-[on #509fa7][/on #2c7585]+[on #3d6a7a][/on #509fa7]-[on #0d112b][/on #3d6a7a] [on #0d132b][/on #0d112b] [on #0b0e27][/on #0d132b] [on #162039][/on #0b0e27] [on #376676][/on #162039]-[on #4c8a91][/on #376676]=[on #257382][/on #4c8a91]-[on #70cec1][/on #257382]*[on #8ee9d1][/on #70cec1]#[on #8fd0c0][/on #8ee9d1]#[on #94dac9][/on #8fd0c0]#[on #9fd4c9][/on #94dac9]#[on #428590][/on #9fd4c9]=[on #0b445a][/on #428590] [on #1b4f57][/on #0b445a] [on #154d4d][/on #1b4f57] [on #020a0b][/on #154d4d] [on #010505][/on #020a0b] [on #000000][/on #010505] [on #000102][/on #000000] [on #000000][/on #000102] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] -[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #010203][/on #000000] [on #000001][/on #010203] [on #040809][/on #000001] [on #0a1117][/on #040809] [on #132535][/on #0a1117] [on #0d5067][/on #132535] [on #3a97a3][/on #0d5067]=[on #aceedb][/on #3a97a3]%[on #7ce3cf][/on #aceedb]#[on #6ad8ca][/on #7ce3cf]#[on #64cdc6][/on #6ad8ca]*[on #55b1b0][/on #64cdc6]+[on #7ed9cc][/on #55b1b0]#[on #2b7286][/on #7ed9cc]-[on #276a7f][/on #2b7286]-[on #41a6b0][/on #276a7f]+[on #32848e][/on #41a6b0]=[on #0e0e28][/on #32848e] [on #12233c][/on #0e0e28] [on #30939e][/on #12233c]=[on #3a8291][/on #30939e]=[on #276278][/on #3a8291]-[on #4a9ea2][/on #276278]+[on #73cdc1][/on #4a9ea2]*[on #63b5b1][/on #73cdc1]*[on #66cabf][/on #63b5b1]*[on #77e2cd][/on #66cabf]#[on #94ebd2][/on #77e2cd]%[on #c3eedb][/on #94ebd2]%[on #207b8b][/on #c3eedb]-[on #0d4154][/on #207b8b] [on #0e171f][/on #0d4154] [on #071012][/on #0e171f] [on #030b0d][/on #071012] [on #000001][/on #030b0d] [on #000202][/on #000001] [on #000000][/on #000202] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [bold]Using Libraries[/bold] -[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #020405][/on #000000] [on #000000][/on #020405] [on #0b1520][/on #000000] [on #0d304a][/on #0b1520] [on #195267][/on #0d304a] [on #96cdc3][/on #195267]#[on #84ddcf][/on #96cdc3]#[on #87e4d0][/on #84ddcf]#[on #50c1bf][/on #87e4d0]*[on #3fbabc][/on #50c1bf]+[on #40949a][/on #3fbabc]=[on #54b0ad][/on #40949a]+[on #77ecda][/on #54b0ad]#[on #318896][/on #77ecda]=[on #24637c][/on #318896]-[on #318c9b][/on #24637c]=[on #101935][/on #318c9b] [on #193d56][/on #101935] [on #29788e][/on #193d56]-[on #1d5973][/on #29788e] [on #4cacad][/on #1d5973]+[on #85f2dd][/on #4cacad]%[on #36868d][/on #85f2dd]=[on #44a8ac][/on #36868d]+[on #35adb5][/on #44a8ac]+[on #63d0c6][/on #35adb5]*[on #89e3d0][/on #63d0c6]#[on #a5eed9][/on #89e3d0]%[on #84adaa][/on #a5eed9]*[on #00314d][/on #84adaa] [on #122637][/on #00314d] [on #070d13][/on #122637] [on #000000][/on #070d13] [on #020405][/on #000000] [on #000000][/on #020405] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] - [b]scapy[/b] : [white]{scapy_version}[/white] -[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #071919][/on #000000] [on #0e3333][/on #071919] [on #06050e][/on #0e3333] [on #0c3047][/on #06050e] [on #548086][/on #0c3047]=[on #7ad7c8][/on #548086]#[on #4db2b3][/on #7ad7c8]+[on #8ee2d1][/on #4db2b3]#[on #40b3b8][/on #8ee2d1]+[on #2b8b9c][/on #40b3b8]=[on #49aeb2][/on #2b8b9c]+[on #22576a][/on #49aeb2] [on #40a6ac][/on #22576a]+[on #59d6cf][/on #40a6ac]*[on #2c8799][/on #59d6cf]=[on #1d4b68][/on #2c8799] [on #11203c][/on #1d4b68] [on #122541][/on #11203c] [on #1b526f][/on #122541] [on #339da8][/on #1b526f]=[on #54d1cb][/on #339da8]*[on #2a7984][/on #54d1cb]-[on #316e79][/on #2a7984]-[on #399ea9][/on #316e79]+[on #1e7f95][/on #399ea9]-[on #57c9c4][/on #1e7f95]*[on #8cd6c7][/on #57c9c4]#[on #50c0bd][/on #8cd6c7]*[on #8cd8cb][/on #50c0bd]#[on #315e6e][/on #8cd8cb]-[on #0b263e][/on #315e6e] [on #040208][/on #0b263e] [on #0d3334][/on #040208] [on #040d0e][/on #0d3334] [on #000000][/on #040d0e] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] - [b]impacket[/b] : [white]{impacket_version}[/white] -[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000101][/on #000000] [on #010404][/on #000101] [on #1c5b58][/on #010404] [on #011416][/on #1c5b58] [on #1e404b][/on #011416] [on #5eb0ae][/on #1e404b]+[on #49bcb9][/on #5eb0ae]*[on #37a0ab][/on #49bcb9]+[on #56a7a9][/on #37a0ab]+[on #4fc7c7][/on #56a7a9]*[on #216d85][/on #4fc7c7]-[on #256e82][/on #216d85]-[on #36949f][/on #256e82]=[on #183c58][/on #36949f] [on #3193a0][/on #183c58]=[on #39abb4][/on #3193a0]+[on #15334d][/on #39abb4] [on #11233c][/on #15334d] [on #101d38][/on #11233c] [on #1f5a72][/on #101d38] [on #3cb7bc][/on #1f5a72]+[on #1f6b7d][/on #3cb7bc]-[on #19455c][/on #1f6b7d] [on #3498a3][/on #19455c]=[on #1c5468][/on #3498a3] [on #218096][/on #1c5468]=[on #6dd0cc][/on #218096]*[on #4d9198][/on #6dd0cc]=[on #47bdbf][/on #4d9198]*[on #53c2c1][/on #47bdbf]*[on #54a3a5][/on #53c2c1]+[on #062131][/on #54a3a5] [on #072426][/on #062131] [on #175354][/on #072426] [on #000001][/on #175354] [on #000101][/on #000001] [on #000000][/on #000101] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] - [b]aiosmtpd[/b] : [white]{aiosmtpd_version}[/white] -[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #030908][/on #000000] [on #2d6364][/on #030908]-[on #69acab][/on #2d6364]+[on #45bfbe][/on #69acab]*[on #2c8897][/on #45bfbe]=[on #37a1ad][/on #2c8897]+[on #25697e][/on #37a1ad]-[on #52aaad][/on #25697e]+[on #35a6b1][/on #52aaad]+[on #112c46][/on #35a6b1] [on #1f4b61][/on #112c46] [on #318393][/on #1f4b61]=[on #1c4762][/on #318393] [on #2c7f90][/on #1c4762]=[on #152e46][/on #2c7f90] [on #142540][/on #152e46] [on #101a35][/on #142540] [on #184058][/on #101a35] [on #256a7d][/on #184058]-[on #1e546b][/on #256a7d] [on #257a85][/on #1e546b]-[on #0a2230][/on #257a85] [on #154655][/on #0a2230] [on #46bfc2][/on #154655]*[on #46838b][/on #46bfc2]=[on #1b788c][/on #46838b]-[on #3baab3][/on #1b788c]+[on #35a1aa][/on #3baab3]+[on #55cac6][/on #35a1aa]*[on #61a4a3][/on #55cac6]+[on #215657][/on #61a4a3] [on #020605][/on #215657] [on #000000][/on #020605] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] - [b]aioquic[/b] : [white]{aioquic_version}[/white] -[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000101][/on #000000] [on #000000][/on #000101] [on #0b1b21][/on #000000] [on #3b7679][/on #0b1b21]-[on #328a8e][/on #3b7679]=[on #2ba3af][/on #328a8e]+[on #268396][/on #2ba3af]=[on #1b4865][/on #268396] [on #298193][/on #1b4865]=[on #245f72][/on #298193]-[on #2ea2ae][/on #245f72]+[on #258188][/on #2ea2ae]=[on #0d0a23][/on #258188] [on #142f43][/on #0d0a23] [on #20546e][/on #142f43] [on #1b3c5c][/on #20546e] [on #18314a][/on #1b3c5c] [on #13243f][/on #18314a] [on #101b36][/on #13243f] [on #102442][/on #101b36] [on #1a4966][/on #102442] [on #1e616e][/on #1a4966]-[on #080f1c][/on #1e616e] [on #0e1223][/on #080f1c] [on #38a7ac][/on #0e1223]+[on #298796][/on #38a7ac]=[on #185366][/on #298796] [on #258295][/on #185366]=[on #1d5370][/on #258295] [on #299ca9][/on #1d5370]=[on #2b9faa][/on #299ca9]=[on #419491][/on #2b9faa]=[on #407173][/on #419491]-[on #071013][/on #407173] [on #000101][/on #071013] [on #000000][/on #000101] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] - [b]pyipp[/b] : [white]{pyipp_version}[/white] +[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #010203][/on #000000] [on #000001][/on #010203] [on #040809][/on #000001] [on #0a1117][/on #040809] [on #132535][/on #0a1117] [on #0d5067][/on #132535] [on #3a97a3][/on #0d5067]=[on #aceedb][/on #3a97a3]%[on #7ce3cf][/on #aceedb]#[on #6ad8ca][/on #7ce3cf]#[on #64cdc6][/on #6ad8ca]*[on #55b1b0][/on #64cdc6]+[on #7ed9cc][/on #55b1b0]#[on #2b7286][/on #7ed9cc]-[on #276a7f][/on #2b7286]-[on #41a6b0][/on #276a7f]+[on #32848e][/on #41a6b0]=[on #0e0e28][/on #32848e] [on #12233c][/on #0e0e28] [on #30939e][/on #12233c]=[on #3a8291][/on #30939e]=[on #276278][/on #3a8291]-[on #4a9ea2][/on #276278]+[on #73cdc1][/on #4a9ea2]*[on #63b5b1][/on #73cdc1]*[on #66cabf][/on #63b5b1]*[on #77e2cd][/on #66cabf]#[on #94ebd2][/on #77e2cd]%[on #c3eedb][/on #94ebd2]%[on #207b8b][/on #c3eedb]-[on #0d4154][/on #207b8b] [on #0e171f][/on #0d4154] [on #071012][/on #0e171f] [on #030b0d][/on #071012] [on #000001][/on #030b0d] [on #000202][/on #000001] [on #000000][/on #000202] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [bold white]Using Libraries[/bold white] +[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #020405][/on #000000] [on #000000][/on #020405] [on #0b1520][/on #000000] [on #0d304a][/on #0b1520] [on #195267][/on #0d304a] [on #96cdc3][/on #195267]#[on #84ddcf][/on #96cdc3]#[on #87e4d0][/on #84ddcf]#[on #50c1bf][/on #87e4d0]*[on #3fbabc][/on #50c1bf]+[on #40949a][/on #3fbabc]=[on #54b0ad][/on #40949a]+[on #77ecda][/on #54b0ad]#[on #318896][/on #77ecda]=[on #24637c][/on #318896]-[on #318c9b][/on #24637c]=[on #101935][/on #318c9b] [on #193d56][/on #101935] [on #29788e][/on #193d56]-[on #1d5973][/on #29788e] [on #4cacad][/on #1d5973]+[on #85f2dd][/on #4cacad]%[on #36868d][/on #85f2dd]=[on #44a8ac][/on #36868d]+[on #35adb5][/on #44a8ac]+[on #63d0c6][/on #35adb5]*[on #89e3d0][/on #63d0c6]#[on #a5eed9][/on #89e3d0]%[on #84adaa][/on #a5eed9]*[on #00314d][/on #84adaa] [on #122637][/on #00314d] [on #070d13][/on #122637] [on #000000][/on #070d13] [on #020405][/on #000000] [on #000000][/on #020405] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [white]- [b]scapy[/b] : {scapy_version}[/white] +[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #071919][/on #000000] [on #0e3333][/on #071919] [on #06050e][/on #0e3333] [on #0c3047][/on #06050e] [on #548086][/on #0c3047]=[on #7ad7c8][/on #548086]#[on #4db2b3][/on #7ad7c8]+[on #8ee2d1][/on #4db2b3]#[on #40b3b8][/on #8ee2d1]+[on #2b8b9c][/on #40b3b8]=[on #49aeb2][/on #2b8b9c]+[on #22576a][/on #49aeb2] [on #40a6ac][/on #22576a]+[on #59d6cf][/on #40a6ac]*[on #2c8799][/on #59d6cf]=[on #1d4b68][/on #2c8799] [on #11203c][/on #1d4b68] [on #122541][/on #11203c] [on #1b526f][/on #122541] [on #339da8][/on #1b526f]=[on #54d1cb][/on #339da8]*[on #2a7984][/on #54d1cb]-[on #316e79][/on #2a7984]-[on #399ea9][/on #316e79]+[on #1e7f95][/on #399ea9]-[on #57c9c4][/on #1e7f95]*[on #8cd6c7][/on #57c9c4]#[on #50c0bd][/on #8cd6c7]*[on #8cd8cb][/on #50c0bd]#[on #315e6e][/on #8cd8cb]-[on #0b263e][/on #315e6e] [on #040208][/on #0b263e] [on #0d3334][/on #040208] [on #040d0e][/on #0d3334] [on #000000][/on #040d0e] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [white]- [b]impacket[/b] : {impacket_version}[/white] +[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000101][/on #000000] [on #010404][/on #000101] [on #1c5b58][/on #010404] [on #011416][/on #1c5b58] [on #1e404b][/on #011416] [on #5eb0ae][/on #1e404b]+[on #49bcb9][/on #5eb0ae]*[on #37a0ab][/on #49bcb9]+[on #56a7a9][/on #37a0ab]+[on #4fc7c7][/on #56a7a9]*[on #216d85][/on #4fc7c7]-[on #256e82][/on #216d85]-[on #36949f][/on #256e82]=[on #183c58][/on #36949f] [on #3193a0][/on #183c58]=[on #39abb4][/on #3193a0]+[on #15334d][/on #39abb4] [on #11233c][/on #15334d] [on #101d38][/on #11233c] [on #1f5a72][/on #101d38] [on #3cb7bc][/on #1f5a72]+[on #1f6b7d][/on #3cb7bc]-[on #19455c][/on #1f6b7d] [on #3498a3][/on #19455c]=[on #1c5468][/on #3498a3] [on #218096][/on #1c5468]=[on #6dd0cc][/on #218096]*[on #4d9198][/on #6dd0cc]=[on #47bdbf][/on #4d9198]*[on #53c2c1][/on #47bdbf]*[on #54a3a5][/on #53c2c1]+[on #062131][/on #54a3a5] [on #072426][/on #062131] [on #175354][/on #072426] [on #000001][/on #175354] [on #000101][/on #000001] [on #000000][/on #000101] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [white]- [b]aiosmtpd[/b] : {aiosmtpd_version}[/white] +[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #030908][/on #000000] [on #2d6364][/on #030908]-[on #69acab][/on #2d6364]+[on #45bfbe][/on #69acab]*[on #2c8897][/on #45bfbe]=[on #37a1ad][/on #2c8897]+[on #25697e][/on #37a1ad]-[on #52aaad][/on #25697e]+[on #35a6b1][/on #52aaad]+[on #112c46][/on #35a6b1] [on #1f4b61][/on #112c46] [on #318393][/on #1f4b61]=[on #1c4762][/on #318393] [on #2c7f90][/on #1c4762]=[on #152e46][/on #2c7f90] [on #142540][/on #152e46] [on #101a35][/on #142540] [on #184058][/on #101a35] [on #256a7d][/on #184058]-[on #1e546b][/on #256a7d] [on #257a85][/on #1e546b]-[on #0a2230][/on #257a85] [on #154655][/on #0a2230] [on #46bfc2][/on #154655]*[on #46838b][/on #46bfc2]=[on #1b788c][/on #46838b]-[on #3baab3][/on #1b788c]+[on #35a1aa][/on #3baab3]+[on #55cac6][/on #35a1aa]*[on #61a4a3][/on #55cac6]+[on #215657][/on #61a4a3] [on #020605][/on #215657] [on #000000][/on #020605] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [white]- [b]aioquic[/b] : {aioquic_version}[/white] +[on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000101][/on #000000] [on #000000][/on #000101] [on #0b1b21][/on #000000] [on #3b7679][/on #0b1b21]-[on #328a8e][/on #3b7679]=[on #2ba3af][/on #328a8e]+[on #268396][/on #2ba3af]=[on #1b4865][/on #268396] [on #298193][/on #1b4865]=[on #245f72][/on #298193]-[on #2ea2ae][/on #245f72]+[on #258188][/on #2ea2ae]=[on #0d0a23][/on #258188] [on #142f43][/on #0d0a23] [on #20546e][/on #142f43] [on #1b3c5c][/on #20546e] [on #18314a][/on #1b3c5c] [on #13243f][/on #18314a] [on #101b36][/on #13243f] [on #102442][/on #101b36] [on #1a4966][/on #102442] [on #1e616e][/on #1a4966]-[on #080f1c][/on #1e616e] [on #0e1223][/on #080f1c] [on #38a7ac][/on #0e1223]+[on #298796][/on #38a7ac]=[on #185366][/on #298796] [on #258295][/on #185366]=[on #1d5370][/on #258295] [on #299ca9][/on #1d5370]=[on #2b9faa][/on #299ca9]=[on #419491][/on #2b9faa]=[on #407173][/on #419491]-[on #071013][/on #407173] [on #000101][/on #071013] [on #000000][/on #000101] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [white]- [b]pyipp[/b] : {pyipp_version}[/white] [on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #010202][/on #000000] [on #000101][/on #010202] [on #081214][/on #000101] [on #051717][/on #081214] [on #092427][/on #051717] [on #133a48][/on #092427] [on #247d90][/on #133a48]-[on #225e77][/on #247d90]-[on #255e75][/on #225e77]-[on #226175][/on #255e75]-[on #1c657a][/on #226175]-[on #288698][/on #1c657a]=[on #122336][/on #288698] [on #165458][/on #122336] [on #060e18][/on #165458] [on #0d2037][/on #060e18] [on #173553][/on #0d2037] [on #14243f][/on #173553] [on #152542][/on #14243f] [on #173454][/on #152542] [on #10243b][/on #173454] [on #0c232b][/on #10243b] [on #17545f][/on #0c232b] [on #193753][/on #17545f] [on #2e9ba9][/on #193753]=[on #154f65][/on #2e9ba9] [on #236072][/on #154f65]-[on #2d788b][/on #236072]-[on #277087][/on #2d788b]-[on #227f8d][/on #277087]-[on #0d2c37][/on #227f8d] [on #052323][/on #0d2c37] [on #071516][/on #052323] [on #070e0f][/on #071516] [on #000000][/on #070e0f] [on #000101][/on #000000] [on #000000][/on #000101] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #030b0b][/on #000000] [on #0a1e1e][/on #030b0b] [on #000000][/on #0a1e1e] [on #061015][/on #000000] [on #1a4b5e][/on #061015] [on #1f5160][/on #1a4b5e] [on #2a7786][/on #1f5160]-[on #206078][/on #2a7786]-[on #267d90][/on #206078]-[on #0e1831][/on #267d90] [on #144749][/on #0e1831] [on #17474f][/on #144749] [on #030209][/on #17474f] [on #0b1626][/on #030209] [on #152640][/on #0b1626] [on #0f1e31][/on #152640] [on #071018][/on #0f1e31] [on #03060d][/on #071018] [on #217172][/on #03060d]-[on #0d2a2f][/on #217172] [on #133249][/on #0d2a2f] [on #22788f][/on #133249]-[on #267386][/on #22788f]-[on #338187][/on #267386]=[on #22586a][/on #338187] [on #195364][/on #22586a] [on #040d0e][/on #195364] [on #000000][/on #040d0e] [on #0c2728][/on #000000] [on #000303][/on #0c2728] [on #000000][/on #000303] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] [on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000102][/on #000000] [on #000000][/on #000102] [on #000000][/on #000000] [on #030609][/on #000000] [on #050e10][/on #030609] [on #1c4a58][/on #050e10] [on #277080][/on #1c4a58]-[on #236372][/on #277080]-[on #0b1d1f][/on #236372] [on #0a1c23][/on #0b1d1f] [on #111e39][/on #0a1c23] [on #070d16][/on #111e39] [on #000000][/on #070d16] [on #000000][/on #000000] [on #0b1728][/on #000000] [on #10263f][/on #0b1728] [on #0c2426][/on #10263f] [on #122b2e][/on #0c2426] [on #2a6f7a][/on #122b2e]-[on #266779][/on #2a6f7a]-[on #16414a][/on #266779] [on #020709][/on #16414a] [on #030d13][/on #020709] [on #000000][/on #030d13] [on #000000][/on #000000] [on #000202][/on #000000] [on #000000][/on #000202] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [on #000000][/on #000000] [/on #000000] diff --git a/dementor/config/session.py b/dementor/config/session.py index d022422..40e3a1a 100644 --- a/dementor/config/session.py +++ b/dementor/config/session.py @@ -218,7 +218,10 @@ def __getitem__(self, key: str) -> Any: def set_interface(self, interface: str) -> None: self.interface = interface self.ipv4 = get_if_addr(self.interface) - self.ipv6 = next( - (ip[0] for ip in in6_getifaddr() if ip[2] == self.interface), - None, - ) + try: + self.ipv6 = next( + (ip[0] for ip in in6_getifaddr() if ip[2] == self.interface), + None, + ) + except Exception: + self.ipv6 = None # see issue #40 diff --git a/dementor/filters.py b/dementor/filters.py index 3e6bc8d..4e963d4 100644 --- a/dementor/filters.py +++ b/dementor/filters.py @@ -22,7 +22,7 @@ import sys import glob import pathlib -import warnings +import fnmatch from typing import Any @@ -63,12 +63,7 @@ def __init__(self, target: str, extra: dict[str, Any] | None = None) -> None: self.target = self.target[2:] # glob.translate is only available since 3.13 if (sys.version_info.major, sys.version_info.minor) < (3, 13): - warnings.warn( - "glob.translate is only available since 3.13, " - + "using basic-string instead", - stacklevel=2, - ) - self.pattern = None + self.pattern = re.compile(fnmatch.translate(self.target)) else: self.pattern = re.compile(glob.translate(self.target)) else: diff --git a/dementor/log/logger.py b/dementor/log/logger.py index e728a34..9796a7d 100644 --- a/dementor/log/logger.py +++ b/dementor/log/logger.py @@ -476,7 +476,10 @@ def add_logfile(self, log_file_path: str) -> None: handler.setFormatter(formatter) self.logger.addHandler(handler) - self.logger.info(f"Created log file handler for {log_file_path}") + self.success( + f"Logging session to {log_file_path}", host=" ", port=" ", protocol="LOG" + ) + # self.logger.info(f"Created log file handler for {log_file_path}") @staticmethod def init_logfile(session: SessionConfig) -> None: diff --git a/dementor/log/stream.py b/dementor/log/stream.py index 53457c0..b9bda36 100644 --- a/dementor/log/stream.py +++ b/dementor/log/stream.py @@ -204,7 +204,12 @@ def __init__(self, path: str | Path, config: HostsStreamConfig) -> None: self.hosts: set[str] = set() self.ipv4: bool = config.log_ipv4 self.ipv6: bool = config.log_ipv6 - dm_logger.info(f"Logging host IPs to {path} (IPv4={self.ipv4}, IPv6={self.ipv6})") + dm_logger.success( + f"Logging host IPs to {path} (IPv4={self.ipv4}, IPv6={self.ipv6})", + host=" ", + port=" ", + protocol="LOG", + ) @override def add(self, **kwargs: Any) -> None: @@ -259,7 +264,9 @@ def __init__(self, path: str | Path, config: DNSNamesStreamConfig) -> None: """ super().__init__(path, config) self.hosts: dict[str, set[str]] = defaultdict(set) - dm_logger.info(f"Logging DNS names to {path}") + dm_logger.success( + f"Logging DNS names to {path}", host=" ", port=" ", protocol="LOG" + ) @override def add(self, **kwargs: Any) -> None: @@ -325,7 +332,12 @@ def __init__(self, path: str | Path, config: HashesStreamConfig) -> None: super().__init__(path if not config.split else "/dev/null", config) self.path: Path = Path(path) self.start_time: str = util.now() - dm_logger.info(f"Logging hashes to {path} (split files: {config.split})") + dm_logger.success( + f"Logging hashes to {path} (split files: {config.split})", + host=" ", + port=" ", + protocol="LOG", + ) @override def add(self, **kwargs: Any) -> None: diff --git a/dementor/protocols/http.py b/dementor/protocols/http.py index 92e16bb..ddd7f60 100644 --- a/dementor/protocols/http.py +++ b/dementor/protocols/http.py @@ -96,15 +96,11 @@ def set_proxy_script( path = pathlib.Path(script["file"]) if not path.exists() or not path.is_file(): - dm_logger.error( - f"WPAD script at {path} does not exist - ignoring..." - ) + dm_logger.error(f"WPAD script at {path} does not exist - ignoring...") return if not path.is_file(): - dm_logger.error( - f"WPAD script at {path} is not a file - ignoring..." - ) + dm_logger.error(f"WPAD script at {path} is not a file - ignoring...") return self.proxy_script = path.read_text() @@ -341,9 +337,7 @@ def version_string(self) -> str: def log_message(self, format: str, *args) -> None: # let us log mssages text = format % args - msg = text.translate( - self._control_char_table - ) # ty:ignore[unresolved-attribute] + msg = text.translate(self._control_char_table) # ty:ignore[unresolved-attribute] self.logger.debug(msg) def send_response(self, code: int, message: str | None = None) -> None: @@ -514,9 +508,7 @@ def auth_ntlm( case _: logger.fail(f"Invalid negotiate authentication: {token}") - self.send_error( - HTTPStatus.INTERNAL_SERVER_ERROR, "Internal Server Error" - ) + self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, "Internal Server Error") def auth_bearer(self, token: str, logger: "ProtocolLogger") -> None: self.display_request("Bearer", logger) diff --git a/dementor/protocols/ldap.py b/dementor/protocols/ldap.py index 0e79164..6ea1009 100644 --- a/dementor/protocols/ldap.py +++ b/dementor/protocols/ldap.py @@ -120,9 +120,6 @@ from collections.abc import Callable -# =========================================================================== -# LDAP Server Capabilities (RFC 4512 §5.1, MS-ADTS §3.1.1.3.3) -# =========================================================================== # Per RFC 4512 §5.1: The root DSE's 'supportedCapabilities' attribute lists # the OIDs of capabilities that the server supports. These OIDs identify # features beyond the base LDAP protocol. @@ -145,9 +142,6 @@ "1.2.840.113556.1.4.1670", ] -# =========================================================================== -# SASL Mechanisms (RFC 4513 §5.2.1.5, MS-ADTS §5.1.1.1) -# =========================================================================== # Per RFC 4513 §5.2.1.5: The root DSE's 'supportedSASLMechanisms' attribute # lists the SASL mechanisms the server supports for authentication. # @@ -175,9 +169,6 @@ "NTLM", ] -# =========================================================================== -# Channel Binding Types (RFC 5929 §4) -# =========================================================================== # Per RFC 5929: Channel bindings allow authentication mechanisms to bind # to the underlying secure channel (TLS), preventing man-in-the-middle attacks. # @@ -194,9 +185,6 @@ "tls-server-end-point", ] -# =========================================================================== -# SASL Quality of Protection (RFC 4513 §5.2.4) -# =========================================================================== # Per RFC 4513 §5.2.4: SASL mechanisms can negotiate a security layer that # provides integrity protection and/or confidentiality for subsequent LDAP # operations after successful authentication. @@ -219,9 +207,6 @@ "auth-conf", ] -# =========================================================================== -# DIGEST-MD5 Constants (RFC 2831) -# =========================================================================== # Per RFC 2831: DIGEST-MD5 is a SASL mechanism using HTTP Digest Authentication. # NOTE: DIGEST-MD5 is DEPRECATED per RFC 6331 due to security concerns, but # still supported for legacy client compatibility. @@ -239,16 +224,13 @@ DIGEST_MD5_ALGORITHM: str = "md5-sess" # RFC 2831 §2.1.1: Charset directive for username/password encoding. -# "utf-8" is the standard charset per RFC 2831 §2.1.1. +# "utf-8" is the standard and only supported charset. DIGEST_MD5_CHARSET: str = "utf-8" # RFC 2831 §2.1.2.1: Maximum buffer size for integrity/confidentiality layers. # 65536 bytes is a common default value. DIGEST_MD5_MAXBUF: str = "65536" -# =========================================================================== -# LDAP Result Codes (RFC 4511 Appendix A) -# =========================================================================== # Per RFC 4511 Appendix A: Result codes indicate the outcome of LDAP operations. # These are returned in LDAPResult structures (BindResponse, SearchResultDone, etc.). @@ -285,9 +267,6 @@ # Returned for failed simple bind or SASL authentication. LDAP_INVALID_CREDENTIALS: int = 0x31 -# =========================================================================== -# Extended Operation OIDs (RFC 4511 §4.12) -# =========================================================================== # Per RFC 4511 §4.12: Extended operations allow additional functionality # beyond the core LDAP protocol. Each operation is identified by an OID. @@ -298,34 +277,15 @@ # Per RFC 4513 §3.1.2: StartTLS MUST NOT be used during SASL negotiation. LDAP_STARTTLS_OID: str = "1.3.6.1.4.1.1466.20037" -# =========================================================================== -# Default Domain -# =========================================================================== - DEFAULT_DOMAIN: str = "example.com" -# =========================================================================== -# Exceptions -# =========================================================================== class LDAPTerminateSession(Exception): - """Exception raised to terminate an LDAP session gracefully. - - This exception is used internally to signal that an LDAP session should - be terminated cleanly after sending a final response to the client. - """ + """Raised internally to terminate an LDAP session after the final response is sent.""" -# =========================================================================== -# Configuration -# =========================================================================== class LDAPServerConfig(TomlConfig): - """Configuration class for LDAP server settings. - - Defines configurable parameters for LDAP server behavior, including - port, TLS settings, supported capabilities, SASL mechanisms, and - channel binding/signing options. - """ + """Configurable LDAP server settings (port, TLS, SASL, capabilities, signing).""" _section_ = "LDAP" _fields_ = [ @@ -395,11 +355,7 @@ class LDAPServerConfig(TomlConfig): ntlm_disable_ntlmv2: bool def set_ldap_error_code(self, value: str | int) -> None: - """Set the LDAP error code for bind responses. - - :param value: Error code as integer or named string value - :type value: str | int - """ + """Set the LDAP error code for bind responses.""" if isinstance(value, int): self.ldap_error_code = value else: @@ -409,18 +365,11 @@ def service_name(self) -> str: return ("C" if self.ldap_udp else "") + "LDAP" + ("S" if self.use_ssl else "") def _parse_domain_to_dn(self, domain: str) -> str: - """Parse a domain name into LDAP DN format. - - Converts DNS-style domain names into LDAP Distinguished Name format - by splitting on dots and creating DC (Domain Component) attributes. + """Convert a DNS-style domain name into LDAP DN format. + Splits on dots into DC (Domain Component) attributes: - "example.com" -> "DC=example,DC=com" - "sub.example.com" -> "DC=sub,DC=example,DC=com" - - :param domain: DNS domain name to convert - :type domain: str - :return: LDAP DN format string - :rtype: str """ if not domain: return "" @@ -428,9 +377,6 @@ def _parse_domain_to_dn(self, domain: str) -> str: return ",".join(f"DC={part}" for part in parts if part) -# =========================================================================== -# SASL State Machine (RFC 4513 §5.2.1.2) -# =========================================================================== class SASLAuthState(Enum): """SASL authentication state machine states per RFC 4513 §5.2.1.2. @@ -446,42 +392,31 @@ class SASLAuthState(Enum): - Any state -> FAILED: Authentication fails """ - # Initial state before any SASL exchange INITIAL = "initial" - # Server has sent a challenge to the client - # Per RFC 4513 §5.2.1.2: Server returns saslBindInProgress (14) + # Per RFC 4513 §5.2.1.2: server returns saslBindInProgress (14) CHALLENGE_SENT = "challenge_sent" - # Client has sent a response to the server's challenge RESPONSE_RECEIVED = "response_received" - # Authentication completed successfully - # Per RFC 4513 §5.2.1.2: Server returns success (0) + # Per RFC 4513 §5.2.1.2: server returns success (0) COMPLETE = "complete" - # Authentication failed - # Per RFC 4513 §5.2.1.2: Server returns appropriate error code + # Per RFC 4513 §5.2.1.2: server returns the appropriate error code FAILED = "failed" class SASLMechanismState: - """Tracks state for a single SASL mechanism negotiation. + """Tracks state for a single SASL mechanism negotiation (RFC 4513 §5.2.1). - Per RFC 4513 §5.2.1: SASL authentication may require multiple bind requests - to complete. This class maintains state across those requests. - - Per RFC 4513 §5.2.1.2: The server must track the authentication state and - ensure proper sequencing of challenges and responses. + Per RFC 4513 §5.2.1.2: the server must track the authentication state and + ensure proper sequencing of challenges and responses across bind requests. """ def __init__(self, mechanism: str) -> None: """Initialize SASL mechanism state. Per RFC 4513 §5.2.1.2: Servers SHOULD limit authentication time. - - :param mechanism: Normalized SASL mechanism name (e.g., "GSS_SPNEGO") - :type mechanism: str """ self.mechanism: str = mechanism self.state: SASLAuthState = SASLAuthState.INITIAL @@ -505,9 +440,7 @@ def transition(self, new_state: SASLAuthState) -> None: - RESPONSE_RECEIVED -> FAILED: Authentication fails - COMPLETE/FAILED: Terminal states, no further transitions - :param new_state: Target state to transition to - :type new_state: SASLAuthState - :raises ValueError: If transition is invalid per RFC 4513 §5.2.1.2 + :raises ValueError: If the transition is invalid per RFC 4513 §5.2.1.2. """ valid_transitions: dict[SASLAuthState, list[SASLAuthState]] = { SASLAuthState.INITIAL: [SASLAuthState.CHALLENGE_SENT, SASLAuthState.FAILED], @@ -545,19 +478,16 @@ class AuthorizationState(Enum): - SASL In Progress: Multi-step SASL authentication underway """ - # RFC 4513 §4.1: Anonymous authentication state - # Client has not authenticated or used anonymous bind. - # Access is limited to publicly readable data. + # RFC 4513 §4.1: no authentication performed; access limited to publicly + # readable data. ANONYMOUS = "anonymous" - # RFC 4513 §4.2: Authenticated state - # Client has successfully completed authentication. - # Access is determined by the authenticated identity's permissions. + # RFC 4513 §4.2: access determined by the authenticated identity's + # permissions. AUTHENTICATED = "authenticated" - # RFC 4513 §5.2.1: SASL authentication in progress - # Multi-step SASL mechanism is being negotiated. - # Client has limited access until authentication completes. + # RFC 4513 §5.2.1: multi-step SASL mechanism being negotiated; limited + # access until authentication completes. SASL_IN_PROGRESS = "sasl_in_progress" @@ -590,22 +520,13 @@ def __init__(self) -> None: self.created_at: float = time.time() def authenticate(self, identity: str, method: str) -> None: - """Transition to authenticated state. - - :param identity: Authenticated user identity (DN or username) - :type identity: str - :param method: Authentication method used (e.g., "simple", "sasl") - :type method: str - """ + """Transition to authenticated state.""" self.state = AuthorizationState.AUTHENTICATED self.authenticated_identity = identity self.authentication_method = method def reset_to_anonymous(self) -> None: - """Reset to anonymous state per RFC 4513 §4. - - Clears all authentication state and returns session to anonymous access. - """ + """Reset to anonymous state, clearing all authentication state (RFC 4513 §4).""" self.state = AuthorizationState.ANONYMOUS self.authenticated_identity = None self.authentication_method = None @@ -625,11 +546,7 @@ def enable_sasl(self) -> None: self.sasl_active = True def enable_channel_binding(self, binding_type: str) -> None: - """Enable channel binding on the connection. - - :param binding_type: Type of channel binding (e.g., "tls-unique") - :type binding_type: str - """ + """Enable channel binding on the connection.""" self.channel_binding_active = True self.channel_binding_type = binding_type @@ -650,9 +567,6 @@ def enable_sealing(self) -> None: def apply_negotiated_qop(self, qop: str) -> None: """Set the negotiated quality of protection and activate security layers. - :param qop: Quality of protection ("auth", "auth-int", or "auth-conf") - :type qop: str - Side effects: "auth-int" activates signing; "auth-conf" activates both signing and sealing. """ @@ -664,19 +578,10 @@ def apply_negotiated_qop(self, qop: str) -> None: self.enable_sealing() def is_authenticated(self) -> bool: - """Check if session is in authenticated state. - - :return: True if session is authenticated, False otherwise - :rtype: bool - """ + """Check if session is in authenticated state.""" return self.state == AuthorizationState.AUTHENTICATED -# =========================================================================== -# Server Mixin -# =========================================================================== - - class LDAPServerMixin: """Mixin class providing LDAP server response utilities.""" @@ -686,15 +591,7 @@ class LDAPServerMixin: def search_done( self, req: LDAPMessage, result_code: int = LDAP_SUCCESS ) -> LDAPMessage: - """Generate SearchResultDone message. - - :param req: Original LDAP request message - :type req: LDAPMessage - :param result_code: LDAP result code, defaults to LDAP_SUCCESS - :type result_code: int, optional - :return: SearchResultDone response message - :rtype: LDAPMessage - """ + """Generate SearchResultDone message.""" result = SearchResultDone() result["resultCode"] = result_code result["matchedDN"] = "" @@ -706,15 +603,9 @@ def create_root_dse_entry(self, requested_attrs: list[str]) -> dict[str, list[st Per RFC 4512 §5.1: The root DSE contains server capabilities and configuration information accessible without authentication. - - :param requested_attrs: List of attribute names to include (empty = all) - :type requested_attrs: list[str] - :return: Dictionary mapping attribute names to value lists - :rtype: dict[str, list[str]] """ attributes = {} - # Helper to add attribute if requested def add_if_requested(attr_name: str, values: list[str]) -> None: if ( not requested_attrs @@ -723,7 +614,6 @@ def add_if_requested(attr_name: str, values: list[str]) -> None: ): attributes[attr_name] = values - # Add all standard root DSE attributes add_if_requested("supportedCapabilities", self.server_config.ldap_caps) # Ensure mechanisms are returned in the format expected by Windows clients @@ -748,26 +638,19 @@ def add_if_requested(attr_name: str, values: list[str]) -> None: config_context = f"CN=Configuration,{naming_context}" add_if_requested("configurationNamingContext", [config_context]) - domain_parts = dns_domain + domain_labels = dns_domain.split(".") root_domain = ( - ".".join(domain_parts[-2:]) if len(domain_parts) >= 2 else dns_domain + ".".join(domain_labels[-2:]) if len(domain_labels) >= 2 else dns_domain ) root_context = self.server_config._parse_domain_to_dn(root_domain) add_if_requested("rootDomainNamingContext", [root_context]) - # Add additional AD-specific attributes add_if_requested("supportedLDAPVersion", ["3", "2"]) add_if_requested("namingContexts", [naming_context]) return attributes def search_entry_list(self, entries: dict[str, list[str]]) -> SearchResultEntry: - """Build a SearchResultEntry from attribute dictionary. - - :param entries: Dictionary mapping attribute names to value lists - :type entries: dict[str, list[str]] - :return: Constructed SearchResultEntry message - :rtype: SearchResultEntry - """ + """Build a SearchResultEntry from attribute dictionary.""" search_entry = SearchResultEntry() search_entry["objectName"] = "" @@ -788,19 +671,7 @@ def bind_result( matched_dn: str | bytes | None = None, sasl_credentials: bytes | None = None, ) -> LDAPMessage: - """Generate BindResponse message. - - :param req: Original LDAP bind request message - :type req: LDAPMessage - :param reason: LDAP result code, defaults to LDAP_SUCCESS - :type reason: int, optional - :param matched_dn: Matched DN or SASL challenge data, defaults to None - :type matched_dn: str | bytes | None, optional - :param sasl_credentials: SASL server credentials, defaults to None - :type sasl_credentials: bytes | None, optional - :return: BindResponse message - :rtype: LDAPMessage - """ + """Generate BindResponse message.""" bind = BindResponse() bind["resultCode"] = reason bind["matchedDN"] = matched_dn or "" @@ -816,19 +687,7 @@ def extended_result( response_name: str | None = None, response_value: bytes | None = None, ) -> LDAPMessage: - """Generate ExtendedResponse message. - - :param req: Original LDAP extended request message - :type req: LDAPMessage - :param reason: LDAP result code, defaults to LDAP_SUCCESS - :type reason: int, optional - :param response_name: Response OID, defaults to None - :type response_name: str | None, optional - :param response_value: Response-specific data, defaults to None - :type response_value: bytes | None, optional - :return: ExtendedResponse message - :rtype: LDAPMessage - """ + """Generate ExtendedResponse message.""" extended = ExtendedResponse() extended["resultCode"] = reason extended["matchedDN"] = "" @@ -840,25 +699,13 @@ def extended_result( return self.new_message(req, extended) def new_message(self, req: LDAPMessage, op: Any) -> LDAPMessage: - """Create a new LDAPMessage with the same message ID as the request. - - :param req: Original LDAP request message - :type req: LDAPMessage - :param op: Protocol operation to include in response - :type op: Any - :return: New LDAP message with matching message ID - :rtype: LDAPMessage - """ + """Create a new LDAPMessage with the same message ID as the request.""" message = LDAPMessage() message["messageID"] = req["messageID"] message["protocolOp"].setComponentByType(op.getTagSet(), op) return message -# =========================================================================== -# LDAP Server Classes -# =========================================================================== - ldap_logger = ProtocolLogger( extra={ "protocol": "LDAP", @@ -899,6 +746,7 @@ def generate_self_signed_cert(self) -> None: "Generating self-signed certificate for LDAP server", port=self.server_config.ldap_port, protocol=self.server_config.service_name(), + host=" ", ) cert_path, key_path, temp_dir = generate_self_signed_cert( @@ -925,9 +773,6 @@ def setup_ssl_context(self, transport: socket.socket | None = None) -> None: Loads certificate and key files, wraps the socket with TLS. Generates self-signed certificate if configured and files not found. - - :param transport: Socket to wrap with TLS, defaults to None (uses self.socket) - :type transport: socket.socket | None, optional """ cert_path = self.server_config.certfile key_path = self.server_config.keyfile @@ -999,9 +844,7 @@ def __init__( super().__init__(config, server_address, RequestHandlerClass) -# =========================================================================== # Sicily Bind Response (MS-ADTS §5.1.1.1.3) -# =========================================================================== class SicilyBindResponse(univ.Sequence): @@ -1017,11 +860,6 @@ class SicilyBindResponse(univ.Sequence): ) -# =========================================================================== -# LDAP Handler -# =========================================================================== - - class LDAPHandler(BaseProtoHandler["LDAPServer"]): """Base handler for LDAP protocol messages.""" @@ -1034,17 +872,6 @@ def __init__( client_address: tuple[str, int], server: LDAPServerMixin, ) -> None: - """Initialize the protocol handler. - - :param config: Session configuration object - :type config: SessionConfig - :param request: Client socket or UDP datagram tuple - :type request: socket.socket | tuple[bytes, socket.socket] - :param client_address: Client IP address and port - :type client_address: tuple[str, int] - :param server: LDAP server instance - :type server: LDAPServerMixin - """ self.spnego_negotiator = SPNEGONegotiator( supported_mechs=[SPNEGO_NTLMSSP_MECH], mech_handlers={ @@ -1059,11 +886,6 @@ def __init__( super().__init__(config, request, client_address, server) def proto_logger(self) -> ProtocolLogger: - """Create a protocol logger with LDAP-specific context. - - :return: Configured protocol logger instance - :rtype: ProtocolLogger - """ port: int = self.server.server_config.ldap_port use_ssl: bool = self.server.server_config.use_ssl return ProtocolLogger( @@ -1076,13 +898,6 @@ def proto_logger(self) -> ProtocolLogger: ) def _is_encrypted_message(self, data: bytes) -> bool: - """Detect if received data appears to be an encrypted SASL message. - - :param data: Raw bytes received from client - :type data: bytes - :return: True if data appears to be encrypted SASL message - :rtype: bool - """ if not (self.auth_state.signing_active or self.auth_state.sealing_active): return False @@ -1099,11 +914,9 @@ def _is_encrypted_message(self, data: bytes) -> bool: def recv(self, size: int) -> LDAPMessage | None: # type: ignore[override] # ty:ignore[invalid-method-override] """Receive and decode an LDAP message from the client. - :param size: Maximum bytes to receive (ignored, uses 8192) - :type size: int - :return: Decoded LDAP message or None if connection closed - :rtype: LDAPMessage | None - :raises LDAPTerminateSession: If encrypted SASL message received + The size argument is ignored; always reads up to 8192 bytes. + + :raises LDAPTerminateSession: If an encrypted SASL message is received. """ try: data = super().recv(8192) @@ -1133,11 +946,6 @@ def recv(self, size: int) -> LDAPMessage | None: # type: ignore[override] # ty @override def send(self, data: LDAPMessage | list[LDAPMessage] | None) -> None: # type: ignore[override] # ty:ignore[invalid-method-override] - """Send an LDAP message or list of messages to the client. - - :param data: LDAP message(s) to send, or None to skip - :type data: LDAPMessage | list[LDAPMessage] | None - """ if data is None: return @@ -1164,13 +972,7 @@ def send(self, data: LDAPMessage | list[LDAPMessage] | None) -> None: # type: i @override def handle_data(self, data: bytes | None, transport: socket.socket) -> None: - """Main message processing loop for LDAP connections. - - :param data: Initial data received (unused for LDAP) - :type data: bytes | None - :param transport: Client socket connection - :type transport: socket.socket - """ + """Main message processing loop for LDAP connections.""" if self.server.server_config.ldap_timeout: transport.settimeout(self.server.server_config.ldap_timeout) self.logger.debug( @@ -1212,15 +1014,7 @@ def handle_data(self, data: bytes | None, transport: socket.socket) -> None: def _handle_spnego_ntlm_mech( self, mech_token: bytes | None, is_initiate: bool ) -> tuple[bytes | None, bool]: - """Handle NTLM mechanism within SPNEGO. - - :param mech_token: NTLM token bytes from client - :type mech_token: bytes | None - :param is_initiate: True if this is the initial negotiate phase - :type is_initiate: bool - :return: Tuple of (response_token, negotiation_complete) - :rtype: tuple[bytes | None, bool] - """ + """Handle NTLM mechanism within SPNEGO.""" if is_initiate: if mech_token: token = ntlm.NTLMAuthNegotiate() @@ -1241,7 +1035,6 @@ def _handle_spnego_ntlm_mech( dns_computer=host.get_value(HostValue.DNS_COMPUTER), dns_domain=host.get_value(HostValue.DNS_DOMAIN), # REVISIT: capture DNSTree too - # dns_tree=self.config.ntlm_dns_tree, log=self.logger, ) return ntlm_challenge.getData(), False @@ -1268,11 +1061,6 @@ def _normalize_sasl_mechanism(self, mech_name: str) -> str: - Convert to uppercase (RFC 4422 §3.1: case-insensitive) - Replace hyphens with underscores (Python naming convention) - Strip whitespace - - :param mech_name: Raw SASL mechanism name from client - :type mech_name: str - :return: Normalized mechanism name suitable for method lookup - :rtype: str """ return mech_name.upper().replace("-", "_").strip() @@ -1294,13 +1082,6 @@ def _parse_cleartext_user( 2. UPN format: "user@domain.com" -> (user, domain.com) 3. LDAP DN: "CN=John Doe,DC=example,DC=com" -> (John Doe, None, {dn: ...}) 4. Simple username: "user" -> (user, None) - - :param bind_name: Bind name from LDAP bind request - :type bind_name: str - :return: Tuple of (username, domain, extras_dict) where username is the - extracted username, domain is the extracted domain (None if not - present), and extras contains additional metadata (e.g., full DN) - :rtype: tuple[str, str | None, dict[str, str]] """ extras: dict[str, str] = {} @@ -1341,10 +1122,6 @@ def _parse_cleartext_user( # Simple username with no domain return bind_name, None, extras - # =========================================================================== - # Operation Handlers - # =========================================================================== - def handle_bindRequest( self, message: LDAPMessage, bind_req: BindRequest ) -> LDAPMessage | None: @@ -1363,13 +1140,8 @@ def handle_bindRequest( - name: DN or other identifier (empty for anonymous) - authentication: Simple password, SASL, or other method - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param bind_req: Bind request protocol operation - :type bind_req: BindRequest - :return: ``None`` when the response is sent directly to the client, or - a :class:`LDAPMessage` on the unsupported-version path. - :rtype: LDAPMessage | None + Returns ``None`` when the response is sent directly to the client, or a + :class:`LDAPMessage` on the unsupported-version path. """ self.logger.debug(f"LDAP Bind Request from {self.client_address}") self.logger.debug(f"Bind message ID: {message['messageID']}") @@ -1454,27 +1226,17 @@ def _handle_simple_bind( - Vulnerable to eavesdropping and replay attacks - Should be disabled in production environments without TLS - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param bind_name: Bind DN or username from request - :type bind_name: str - :param bind_password: Password from request - :type bind_password: str - :return: Bind response message - :rtype: LDAPMessage - :raises LDAPTerminateSession: After sending response + :raises LDAPTerminateSession: After sending response. """ self.logger.debug("Processing simple authentication") - # RFC 4513 §5.1.1: Anonymous authentication - # Both name and password are empty -> anonymous bind + # RFC 4513 §5.1.1: Anonymous authentication (empty name and password) if not bind_name and not bind_password: self.logger.debug("Anonymous bind request") return self.server.bind_result(message) - # RFC 4513 §5.1.2: Unauthenticated authentication - # Name is present but password is empty -> treated as anonymous - # Per RFC 4513 §5.1.2: Servers SHOULD NOT grant access based on DN alone + # RFC 4513 §5.1.2: Unauthenticated bind (name present, empty password) is + # treated as anonymous; servers SHOULD NOT grant access based on DN alone. if bind_name and not bind_password: self.logger.debug(f"Unauthenticated bind for DN: {bind_name}") return self.server.bind_result(message) @@ -1538,13 +1300,6 @@ def _handle_sicily_negotiate( - Message type (0x00000001) - Negotiation flags (capabilities) - Optional domain/workstation names - - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param bind_name: Mechanism name from bind request - :type bind_name: str - :param nego_token_raw: Raw NTLM negotiate token bytes - :type nego_token_raw: bytes """ self.logger.debug("Processing Sicily negotiate (NTLM)") @@ -1594,11 +1349,7 @@ def _handle_sicily_response(self, message: LDAPMessage, blob: bytes) -> None: The NTLM response can be cracked offline to recover the password. - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param blob: Raw NTLM authenticate token bytes - :type blob: bytes - :raises LDAPTerminateSession: After sending response + :raises LDAPTerminateSession: After sending response. """ self.logger.debug("Processing Sicily response (NTLM)") if self.mech_name == "ntlm": @@ -1637,11 +1388,6 @@ def _handle_sicily_package_discovery(self, message: LDAPMessage) -> LDAPMessage: - resultCode: 0 for success - serverCreds: Comma or semicolon-separated list of package names - errorMessage: Empty for success - - :param message: LDAP message containing bind request - :type message: LDAPMessage - :return: Sicily bind response with supported packages - :rtype: LDAPMessage """ self.logger.debug("Sicily package discovery: Returning supported packages (NTLM)") @@ -1661,13 +1407,6 @@ def _handle_sasl_bind( Dispatches to mechanism-specific handlers based on the requested SASL mechanism. Manages SASL state machine transitions. - - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param bind_auth: SASL authentication component from bind request - :type bind_auth: Any - :return: Bind response message or None - :rtype: LDAPMessage | None """ mech_name_raw = str(bind_auth["mechanism"]) mech_name = self._normalize_sasl_mechanism(mech_name_raw) @@ -1756,13 +1495,6 @@ def handle_searchRequest( - Root DSE queries (base DN = "") - Basic hostname/DN queries - Does not implement full directory tree traversal - - :param message: LDAP message containing search request - :type message: LDAPMessage - :param search_req: Search request protocol operation - :type search_req: SearchRequest - :return: List of search response messages (entries + done) - :rtype: list[LDAPMessage] """ self.logger.debug(f"LDAP Search Request from {self.client_address}") self.logger.debug(f"Search message ID: {message['messageID']}") @@ -1835,11 +1567,6 @@ def handle_extendedReq( - resultCode: Success/failure indication - responseName: Optional OID (typically echoes request) - responseValue: Optional operation-specific result data - - :param message: LDAP message containing extended request - :type message: LDAPMessage - :param extended_req: Extended request protocol operation - :type extended_req: ExtendedRequest """ self.logger.debug(f"LDAP Extended Request from {self.client_address}") self.logger.debug(f"Extended message ID: {message['messageID']}") @@ -1928,21 +1655,10 @@ def handle_unbindRequest( Per RFC 4511 §4.3: The Unbind operation terminates the LDAP session. No response is sent to the client. - :param message: LDAP message containing unbind request - :type message: LDAPMessage - :param unbind_req: Unbind request protocol operation - :type unbind_req: UnbindRequest - :return: Always raises LDAPTerminateSession - :rtype: bool - :raises LDAPTerminateSession: To terminate the session + :raises LDAPTerminateSession: Always, to terminate the session. """ - # terminate connection raise LDAPTerminateSession - # =========================================================================== - # SASL Mechanism Handlers - # =========================================================================== - def _handle_sasl_GSS_SPNEGO( self, message: LDAPMessage, bind_auth: Any ) -> LDAPMessage | None: @@ -1950,12 +1666,6 @@ def _handle_sasl_GSS_SPNEGO( Processes SPNEGO-wrapped NTLM authentication tokens. - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param bind_auth: SASL authentication component - :type bind_auth: Any - :return: Bind response message or None - :rtype: LDAPMessage | None :raises LDAPTerminateSession: When authentication completes """ data = bytes(bind_auth["credentials"]) @@ -2034,13 +1744,6 @@ def _handle_direct_ntlm( """Fallback handler when SPNEGO parsing fails. Attempts to parse token as direct NTLM without SPNEGO wrapper. - - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param data: Raw token bytes from client - :type data: bytes - :return: Bind response message or None - :rtype: LDAPMessage | None """ try: if not data.startswith(b"NTLMSSP\x00"): @@ -2076,13 +1779,6 @@ def _handle_direct_ntlm( return self.server.bind_result(message, reason=LDAP_AUTH_METHOD_NOT_SUPPORTED) def _handle_ntlm_negotiate(self, message: LDAPMessage, nego_token_raw: bytes) -> None: - """Handle NTLM Negotiate message. - - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param nego_token_raw: Raw NTLM negotiate token bytes - :type nego_token_raw: bytes - """ negotiate = NTLMAuthNegotiate() negotiate.fromString(nego_token_raw) @@ -2097,13 +1793,9 @@ def _handle_ntlm_negotiate(self, message: LDAPMessage, nego_token_raw: bytes) -> self.send(self.server.bind_result(message, matched_dn=ntlm_challenge.getData())) def _handle_ntlm_auth(self, message: LDAPMessage, blob: bytes) -> None: - """Handle NTLM Authenticate message. + """Handle NTLM AUTHENTICATE_MESSAGE and send the bind response. - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param blob: Raw NTLM authenticate token bytes - :type blob: bytes - :raises LDAPTerminateSession: After sending response + :raises LDAPTerminateSession: After sending response. """ auth_message = NTLMAuthChallengeResponse() auth_message.fromString(blob) @@ -2127,10 +1819,6 @@ def _handle_sasl_DIGEST_MD5( ) -> LDAPMessage | None: """Handle SASL DIGEST-MD5 mechanism per RFC 2831. - Per RFC 2831: DIGEST-MD5 is a SASL mechanism based on HTTP Digest Authentication. - NOTE: DIGEST-MD5 is DEPRECATED per RFC 6331 due to security issues, but still - supported for legacy client compatibility. - DIGEST-MD5 Protocol Flow (RFC 2831 §2.1): 1. Client sends empty initial response (or no response) 2. Server sends challenge with realm, nonce, qop, etc. @@ -2155,12 +1843,6 @@ def _handle_sasl_DIGEST_MD5( - digest-uri: Service and host (e.g., "ldap/server.example.com") - response: MD5 digest proving knowledge of password - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param bind_auth: SASL authentication component - :type bind_auth: Any - :return: Bind response message or None - :rtype: LDAPMessage | None :raises LDAPTerminateSession: When authentication completes """ try: @@ -2203,7 +1885,7 @@ def _handle_sasl_DIGEST_MD5( self.server.bind_result( message, reason=LDAP_SASL_BIND_IN_PROGRESS, - sasl_credentials=challenge.encode("utf-8"), # Ensure UTF-8 encoding + sasl_credentials=challenge.encode("utf-8"), ) ) return None @@ -2300,13 +1982,7 @@ def _handle_sasl_DIGEST_MD5( raise LDAPTerminateSession def _parse_digest_response(self, response_str: str) -> dict[str, str] | None: - """Parse DIGEST-MD5 client response string into directive dictionary. - - :param response_str: Raw DIGEST-MD5 response string from client - :type response_str: str - :return: Dictionary of parsed directives or None on error - :rtype: dict[str, str] | None - """ + """Parse DIGEST-MD5 client response string into directive dictionary.""" try: directives = parse_http_list(response_str) parsed = parse_keqv_list(directives) @@ -2324,12 +2000,6 @@ def _handle_sasl_PLAIN( Per RFC 4616: PLAIN transmits credentials in cleartext. MUST only be used over TLS. - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param bind_auth: SASL authentication component - :type bind_auth: Any - :return: Bind response message or None - :rtype: LDAPMessage | None :raises LDAPTerminateSession: After sending response """ credentials = bytes(bind_auth["credentials"]) @@ -2389,15 +2059,7 @@ def _handle_sasl_PLAIN( def _handle_sasl_NTLM( self, message: LDAPMessage, bind_auth: Any ) -> LDAPMessage | None: - """Handle SASL NTLM mechanism (direct, not wrapped in SPNEGO). - - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param bind_auth: SASL authentication component - :type bind_auth: Any - :return: Bind response message or None - :rtype: LDAPMessage | None - """ + """Handle SASL NTLM mechanism (direct, not wrapped in SPNEGO).""" credentials = bytes(bind_auth["credentials"]) self.logger.debug(f"SASL NTLM: Received token, length={len(credentials)}") @@ -2431,15 +2093,7 @@ def _handle_sasl_NTLM( def _handle_sasl_ntlm_negotiate( self, message: LDAPMessage, negotiate_data: bytes ) -> LDAPMessage | None: - """Handle NTLM Negotiate message in SASL NTLM. - - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param negotiate_data: Raw NTLM negotiate token bytes - :type negotiate_data: bytes - :return: Bind response with challenge or error - :rtype: LDAPMessage | None - """ + """Handle NTLM Negotiate message in SASL NTLM.""" try: negotiate = NTLMAuthNegotiate() negotiate.fromString(negotiate_data) @@ -2472,15 +2126,7 @@ def _handle_sasl_ntlm_negotiate( def _handle_sasl_ntlm_authenticate( self, message: LDAPMessage, authenticate_data: bytes ) -> LDAPMessage: - """Handle NTLM Authenticate message in SASL NTLM. - - :param message: LDAP message containing bind request - :type message: LDAPMessage - :param authenticate_data: Raw NTLM authenticate token bytes - :type authenticate_data: bytes - :return: Bind response message - :rtype: LDAPMessage - """ + """Handle NTLM Authenticate message in SASL NTLM.""" try: auth_message = NTLMAuthChallengeResponse() auth_message.fromString(authenticate_data) @@ -2532,11 +2178,6 @@ def _handle_sasl_ntlm_authenticate( CLDAPServer.default_handler_class = LDAPHandler -# =========================================================================== -# Protocol Module -# =========================================================================== - - class LDAP(BaseProtocolModule[LDAPServerConfig]): """LDAP Protocol Module. @@ -2565,15 +2206,7 @@ class LDAP(BaseProtocolModule[LDAPServerConfig]): def create_server_thread( self, session: SessionConfig, server_config: LDAPServerConfig ) -> BaseServerThread[LDAPServerConfig]: - """Create a server thread for LDAP or CLDAP based on configuration. - - :param session: Session configuration object - :type session: SessionConfig - :param server_config: LDAP server configuration - :type server_config: LDAPServerConfig - :return: Configured server thread instance - :rtype: BaseServerThread[LDAPServerConfig] - """ + """Create a server thread for LDAP or CLDAP based on configuration.""" server_cls = CLDAPServer if server_config.ldap_udp else LDAPServer return ServerThread( session, diff --git a/dementor/protocols/msrpc/rpc.py b/dementor/protocols/msrpc/rpc.py index 1acc67e..6052d8a 100644 --- a/dementor/protocols/msrpc/rpc.py +++ b/dementor/protocols/msrpc/rpc.py @@ -385,7 +385,7 @@ def __init__( server_address=None, RequestHandlerClass=None, ) -> None: - self.conn_data = handles or defaultdict(RPCConnection) + self.conn_data = handles if handles is not None else defaultdict(RPCConnection) self._conn_lock = threading.Lock() super().__init__(config, server_address, RequestHandlerClass) diff --git a/dementor/protocols/mssql.py b/dementor/protocols/mssql.py index 0edcfa8..03d3b61 100644 --- a/dementor/protocols/mssql.py +++ b/dementor/protocols/mssql.py @@ -165,6 +165,10 @@ def handle_data(self, data, transport) -> None: if not in_scope(self.client_host, self.config): return + if not data: + # empty/malformed datagram + return + instance_name = self.config.ssrp_config.ssrp_server_instance if data[0] == SSRP_CLNT_UCAST_INST: # request for a specific instance diff --git a/dementor/protocols/mysql.py b/dementor/protocols/mysql.py index 753d14b..031cc29 100644 --- a/dementor/protocols/mysql.py +++ b/dementor/protocols/mysql.py @@ -372,6 +372,10 @@ class HandshakeResponse: # --- MySQL Handler --- class MySQLHandler(BaseProtoHandler): + # AuthPlugin values this handler implements a dispatch method for; any + # other configured value falls back to the default below. + _SUPPORTED_AUTH_PLUGINS: typing.ClassVar[tuple[str, ...]] = ("mysql_clear_password",) + @property def mysql_config(self) -> "MySQLConfig": return self.config.mysql_config @@ -413,8 +417,8 @@ def recv_unpack(self) -> Any: return None def setup(self) -> None: - # plugin = self.config.mysql_config.mysql_plugin_name - plugin = "mysql_clear_password" + plugin = self.config.mysql_config.mysql_plugin_name + # plugin = "mysql_clear_password" self.logger.display( f"New Connection to MySQL Server (requesting handshake for {plugin})" ) @@ -424,18 +428,25 @@ def finish(self) -> None: def handle_data(self, data, transport) -> None: transport.settimeout(2) - # Connection Phase: - # It starts with the client connect()ing to the server which may send a - # ERR packet and finish the handshake or send a Initial Handshake Packet - # which the client answers with a Handshake Response Packet. + # Connection Phase: the server either sends an ERR packet or an + # Initial Handshake Packet, which the client answers with a + # Handshake Response Packet. flags = 0xFFFFFFFF flags ^= CLIENT_CAPABILITY_EXTENSION if not self.mysql_config.use_ssl: flags ^= CLIENT_SSL flags ^= CLIENT_SSL_VERIFY_SERVER_CERT - # NOTE: the configuration value won't have any effect here for now - plugin_name = "mysql_clear_password" + # NOTE: only mysql_clear_password has a dispatch method implemented + # for now + plugin_name = self.mysql_config.mysql_plugin_name + if plugin_name not in self._SUPPORTED_AUTH_PLUGINS: + self.logger.debug( + f"Configured MySQL AuthPlugin {plugin_name!r} is not implemented; " + "falling back to mysql_clear_password" + ) + plugin_name = "mysql_clear_password" + greeting = HandshakeV10( server_version=self.mysql_config.mysql_version, thread_id=10, @@ -452,9 +463,8 @@ def handle_data(self, data, transport) -> None: if packet is None: return - # After this, optionally, the client can request an SSL connection to be established - # with the Protocol::SSLRequest packet and then the client sends the Protocol::HandshakeResponse - # packet. + # The client may now request an SSL upgrade via SSLRequest + # before sending the HandshakeResponse packet. try: ssl_request: SSLRequest = unpack(SSLRequest, packet.payload) except Exception as e: diff --git a/dementor/protocols/netlogon.py b/dementor/protocols/netlogon.py index a7d0efd..9916044 100755 --- a/dementor/protocols/netlogon.py +++ b/dementor/protocols/netlogon.py @@ -32,6 +32,8 @@ - [MS-NRPC] Netlogon Remote Protocol """ +import enum + from scapy.layers import smb from scapy.fields import ( FlagsField, @@ -312,43 +314,45 @@ class NETLOGON_PRIMARY_RESPONSE(smb.NETLOGON): # =========================================================================== # Response Type Selection per [MS-ADTS] § 6.3.5 # =========================================================================== -def select_response_type(nt_version: int) -> int: +class ResponseFamily(enum.Enum): + """Selects which response builder to use for a NETLOGON reply. + + NETLOGON_SAM_LOGON_RESPONSE_EX and NETLOGON_PRIMARY_RESPONSE share the + same on-wire opcode (0x17), so the opcode value alone cannot tell them + apart. This enum is the internal selector `build_response()` dispatches + on; the on-wire opcode is only decided when the chosen builder + constructs its response. """ - Select appropriate NETLOGON response type based on NtVersion flags. - - Per [MS-ADTS] § 6.3.5, the server selects the response structure based - on the NtVersion field in the client's request: - - 1. If dc.nt4EmulatorEnabled is TRUE and AVOID_NT4EMUL is not set, - use NETLOGON_SAM_LOGON_RESPONSE_NT40 - 2. Else if V5EX or V5EX_WITH_IP is set, - use NETLOGON_SAM_LOGON_RESPONSE_EX - 3. Else if V5 is set, - use NETLOGON_SAM_LOGON_RESPONSE - 4. Else if PDC is set, - use NETLOGON_PRIMARY_RESPONSE - 5. Else, - use NETLOGON_SAM_LOGON_RESPONSE_NT40 - - :param nt_version: NtVersion field from client request - :type nt_version: int - :return: Opcode for response structure - :rtype: int + + SAM_LOGON_EX = enum.auto() + SAM_LOGON = enum.auto() + PRIMARY = enum.auto() + SAM_LOGON_NT40 = enum.auto() + + +def select_response_type(nt_version: int) -> ResponseFamily: + """Select the NETLOGON response family for the client's NtVersion flags. + + Per [MS-ADTS] § 6.3.5: SAM_LOGON_EX if V5EX/V5EX_WITH_IP is set, else + SAM_LOGON if V5 is set, else PRIMARY if PDC is set, else SAM_LOGON_NT40. + + NOTE: the spec's other NT40 case (DC has NT4 emulation enabled) is not + implemented, since this server has no such mode. """ # Check for extended V5 support if nt_version & (NETLOGON_NT_VERSION_5EX | NETLOGON_NT_VERSION_5EX_WITH_IP): - return LOGON_SAM_LOGON_RESPONSE_EX + return ResponseFamily.SAM_LOGON_EX # Check for V5 support if nt_version & NETLOGON_NT_VERSION_5: - return LOGON_SAM_LOGON_RESPONSE + return ResponseFamily.SAM_LOGON # Check for PDC query if nt_version & NETLOGON_NT_VERSION_PDC: - return LOGON_PRIMARY_RESPONSE + return ResponseFamily.PRIMARY # Default to NT40 response - return LOGON_SAM_LOGON_RESPONSE # NT40 response (opcode 19) + return ResponseFamily.SAM_LOGON_NT40 # =========================================================================== @@ -709,10 +713,10 @@ def build_response( user_name = getattr(request, "UnicodeUserName", "") # Select response type - response_type: int = select_response_type(nt_version) + response_type = select_response_type(nt_version) # Build appropriate response - if response_type == LOGON_SAM_LOGON_RESPONSE_EX: + if response_type == ResponseFamily.SAM_LOGON_EX: return build_sam_logon_response_ex( dc_name=dc_name, user_name=user_name, @@ -725,7 +729,7 @@ def build_response( flags=flags, ) - if response_type == LOGON_SAM_LOGON_RESPONSE: + if response_type == ResponseFamily.SAM_LOGON: if dc_ip_address: return build_sam_logon_response( dc_name=dc_name, @@ -744,7 +748,7 @@ def build_response( domain_name=domain_name, ) - if response_type == LOGON_PRIMARY_RESPONSE: + if response_type == ResponseFamily.PRIMARY: return build_primary_response( domain_name=domain_name, dc_name=dc_name, diff --git a/dementor/protocols/pop3.py b/dementor/protocols/pop3.py index 4df3dc5..0ff199d 100644 --- a/dementor/protocols/pop3.py +++ b/dementor/protocols/pop3.py @@ -180,11 +180,7 @@ def challenge_auth( self.line(line) resp = self.rfile.readline(1024).strip().decode("utf-8", errors="replace") self.logger.debug(f"C: {resp!r}") - # A client response consists of a line containing a string - # encoded as Base64. If the client wishes to cancel the - # authentication exchange, it issues a line with a single "*". - # If the server receives such a response, it MUST reject the AUTH - # command by sending an -ERR reply. + # [rfc1939] a lone "*" cancels the AUTH exchange and must be rejected. if resp == "*": self.err("Authentication canceled") raise CloseConnection @@ -209,14 +205,14 @@ def handle_data(self, data: bytes, transport) -> None: # ty:ignore[invalid-meth self.logger.debug(f"C: {line!r}") line = line.decode("utf-8", errors="replace").strip() - args = line.split(" ") - if len(args) > 0: - method = getattr(self, f"do_{args[0].upper()}", None) - if method: - try: - method(args[1:]) - except CloseConnection: - break + args = line.split(" ") if line else [] + command = args[0] if args else "" + method = getattr(self, f"do_{command.upper()}", None) if command else None + if method: + try: + method(args[1:]) + except CloseConnection: + break continue self.logger.debug(f"Unknown command: {line!r}") @@ -311,12 +307,7 @@ def auth_PLAIN(self, initial_response: str | None = None) -> None: initial_response = self.challenge_auth(decode=True) try: - # The mechanism consists of a single message, a string of [UTF-8] - # encoded [Unicode] characters, from the client to the server. The - # client presents the authorization identity (identity to act as), - # followed by a NUL (U+0000) character, followed by the authentication - # identity (identity whose password will be used), followed by a NUL - # (U+0000) character, followed by the clear-text password. + # Message format: authzid \0 authcid \0 password _, login, password = initial_response.split("\x00") except ValueError: return self.err("Invalid login data") diff --git a/dementor/protocols/quic.py b/dementor/protocols/quic.py index 6ad14c8..4ceb678 100644 --- a/dementor/protocols/quic.py +++ b/dementor/protocols/quic.py @@ -226,7 +226,7 @@ def is_running(self) -> bool: def generate_self_signed_cert(self) -> None: """Generate a self-signed certificate and private key for QUIC server.""" logger = QuicHandler._make_proto_logger(self.server_config.quic_port) - logger.display("Generating self-signed certificate for QUIC server") + logger.display("Generating self-signed certificate for QUIC server", host=" ") cert_path, key_path, temp_dir = generate_self_signed_cert( self.server_config.quic_cert_cn, diff --git a/dementor/protocols/smb.py b/dementor/protocols/smb.py index 309bf8e..974bc30 100644 --- a/dementor/protocols/smb.py +++ b/dementor/protocols/smb.py @@ -78,8 +78,6 @@ __proto__ = ["SMB"] -# --- Helpers ----------------------------------------------------------------- - def _split_smb_strings(data: bytes, is_unicode: bool) -> list[str]: r"""Split concatenated null-terminated SMB strings from raw bytes. @@ -94,10 +92,6 @@ def _split_smb_strings(data: bytes, is_unicode: bool) -> list[str]: Simple ``split(b"\x00\x00")`` is wrong because ``\x00`` can appear within a valid UTF-16LE code unit at an odd offset. Per [MS-CIFS] §2.2.1.1 (UNICODE_STRING). - - :param data: Raw concatenated null-terminated strings - :param is_unicode: True when FLAGS2_UNICODE is set - :return: List of decoded strings """ if not data: return [] @@ -126,7 +120,6 @@ def _split_smb_strings(data: bytes, is_unicode: bool) -> list[str]: return segments -# --- Constants --------------------------------------------------------------- SMB2_DIALECTS = { smb2.SMB2_DIALECT_002: "SMB 2.002", smb2.SMB2_DIALECT_21: "SMB 2.1", @@ -195,16 +188,8 @@ class SMB2SigningCapabilities(struct_factory.mixin): # type: ignore[unsupported SigningAlgorithms: f[list[int], uint16[this.SigningAlgorithmCount]] -# --- Config ------------------------------------------------------------------ def parse_dialect(value: str | int) -> int: - """Convert a dialect string (e.g. "3.1.1") to its hex constant. - - :param value: Dialect version as a string (e.g. "3.1.1") or integer hex constant - :type value: str | int - :raises ValueError: If the string is not a recognized SMB2 dialect - :return: The SMB2 dialect hex constant - :rtype: int - """ + """Convert a dialect string (e.g. "3.1.1") to its hex constant.""" if isinstance(value, int): return value key = str(value).strip() @@ -225,14 +210,12 @@ class SMBServerConfig(TomlConfig): _section_ = "SMB" _fields_ = [ - # --- Transport & Protocol --- A("smb_port", "Port"), A("smb_enable_smb1", "EnableSMB1", True, factory=is_true), A("smb_enable_smb2", "EnableSMB2", True, factory=is_true), A("smb_allow_smb1_upgrade", "AllowSMB1Upgrade", True, factory=is_true), A("smb2_min_dialect", "SMB2MinDialect", "2.002", factory=parse_dialect), A("smb2_max_dialect", "SMB2MaxDialect", "3.1.1", factory=parse_dialect), - # --- SMB Identity --- A( "smb_nb_computer", "NTLM.NetBIOSComputer", @@ -249,7 +232,6 @@ class SMBServerConfig(TomlConfig): ), A("smb_server_os", "ServerOS", "Windows"), A("smb_native_lanman", "NativeLanMan", "Windows"), - # --- Post-Auth --- A("smb_captures_per_connection", "CapturesPerConnection", 0, factory=int), A("smb_error_code", "ErrorCode", default_val=None), ] @@ -273,10 +255,6 @@ def set_smb_error_code(self, value: str | int | None) -> None: Falls back to STATUS_SMB_BAD_UID if the string does not match any known nt_errors constant. - - :param value: NTSTATUS code as an integer or attribute name string - (e.g. "STATUS_ACCESS_DENIED") - :type value: str | int """ match value: case int(): @@ -304,15 +282,7 @@ class SMB(BaseProtocolModule[SMBServerConfig]): def create_server_thread( self, session: SessionConfig, server_config: SMBServerConfig ) -> BaseServerThread[SMBServerConfig]: - """Create a server thread bound to the configured SMB port. - - :param session: The active session configuration. - :type session: SessionConfig - :param server_config: SMB-specific server configuration from TOML. - :type server_config: SMBServerConfig - :return: A server thread running :class:`SMBServer`. - :rtype: BaseServerThread[SMBServerConfig] - """ + """Create a server thread bound to the configured SMB port.""" return ServerThread( session, server_config, @@ -325,13 +295,8 @@ def create_server_thread( ) -# --- Utilities --------------------------------------------------------------- def get_server_time() -> int: - """Return current UTC time as a Windows FILETIME for SMB timestamps. - - :return: Current UTC time encoded as a 64-bit Windows FILETIME value - :rtype: int - """ + """Return current UTC time as a Windows FILETIME for SMB timestamps.""" return ntlm_timestamp() @@ -340,13 +305,6 @@ def get_command_name(command: int, smb_version: int) -> str: Searches the ``smb.SMB`` constants (for SMBv1) or ``smb2`` module constants (for SMBv2) to find the symbolic name matching the opcode. - - :param command: The SMB command opcode to look up - :type command: int - :param smb_version: SMB protocol version (``0x01`` for SMB1, ``0x02`` for SMB2) - :type smb_version: int - :return: The symbolic command name (e.g. "SMB_COM_NEGOTIATE"), or "Unknown" - :rtype: str """ match smb_version: case 0x01: @@ -363,7 +321,6 @@ def get_command_name(command: int, smb_version: int) -> str: return "Unknown" -# --- Handler ----------------------------------------------------------------- class SMBHandler(BaseProtoHandler): """Per-connection SMB protocol handler for NTLM credential capture. @@ -380,8 +337,6 @@ class SMBHandler(BaseProtoHandler): STATE_NEGOTIATE = 0 STATE_AUTH = 1 - # ══ Connection Lifecycle ════════════════════════════════════════════════════ - def __init__( self, config: SessionConfig, @@ -395,17 +350,6 @@ def __init__( Sets up per-connection state including SMB1/SMB2 session tracking, authentication counters, and command dispatch tables. Delegates to :class:`BaseProtoHandler` for transport setup. - - :param config: The active session configuration - :type config: SessionConfig - :param server_config: SMB-specific server configuration from TOML - :type server_config: SMBServerConfig - :param request: The raw socket/request object from the TCP server - :type request: typing.Any - :param client_address: The ``(host, port)`` tuple of the connecting client - :type client_address: tuple[str, int] - :param server: The parent :class:`SMBServer` instance - :type server: typing.Any """ self.authenticated = False self.smb_config = server_config @@ -486,11 +430,7 @@ def __init__( super().__init__(config, request, client_address, server) def proto_logger(self) -> ProtocolLogger: - """Create a protocol-specific logger with SMB metadata. - - :return: A logger instance tagged with protocol name, color, host, and port - :rtype: ProtocolLogger - """ + """Create a protocol-specific logger with SMB metadata.""" return ProtocolLogger( extra={ "protocol": "SMB", @@ -538,16 +478,8 @@ def _emit_smb_client_info(self) -> None: conn_info = " [b]|[/] ".join(parts) self.logger.display(f"Client connection: {conn_info} closed") - # ══ Transport & Dispatch ════════════════════════════════════════════════════ - def send_data(self, payload: bytes, ty: int | None = None) -> None: - """Wrap payload in a NetBIOS session packet and send it to the client. - - :param payload: The raw bytes to send as the NetBIOS trailer - :type payload: bytes - :param ty: NetBIOS session packet type, defaults to NETBIOS_SESSION_MESSAGE - :type ty: int | None, optional - """ + """Wrap payload in a NetBIOS session packet and send it to the client.""" packet = nmb.NetBIOSSessionPacket() packet.set_type(ty or nmb.NETBIOS_SESSION_MESSAGE) packet.set_trailer(payload) @@ -570,17 +502,6 @@ def send_smb1_command( the legacy ErrorClass/Reserved/ErrorCode fields. Spec: [MS-CIFS] §2.2.3.1 (SMB header), [MS-SMB] §2.2.3.1 (Flags2) - - :param command: The SMB1 command code (e.g. ``smb.SMB.SMB_COM_NEGOTIATE``) - :type command: int - :param data: The SMB command data portion (SMBCommand Data field) - :type data: object - :param parameters: The SMB command parameters portion (SMBCommand Parameters field) - :type parameters: object - :param packet: The original client request packet, used to echo PID/TID/MID - :type packet: smb.NewSMBPacket - :param error_code: NTSTATUS error code for the response, defaults to None (success) - :type error_code: int | None, optional """ resp = smb.NewSMBPacket() # [MS-CIFS] §2.2.3.1: SMB_FLAGS_REPLY (0x80) on server responses @@ -630,23 +551,6 @@ def send_smb2_command( (e.g., for unsolicited NEGOTIATE responses), uses safe defaults. Spec: [MS-SMB2] §2.2.1 (SMB2 header), [MS-SMB2] §3.3.5.5.1 (SessionID) - - :param command_data: The serialized SMB2 command response body - :type command_data: bytes - :param packet: The original client request packet for echoing fields, - defaults to None (uses safe defaults) - :type packet: typing.Any | None, optional - :param command: SMB2 command opcode override when *packet* is None, - defaults to None - :type command: int | None, optional - :param status: NTSTATUS code for the response, defaults to None - (STATUS_SUCCESS) - :type status: int | None, optional - :param tree_id: Override TreeID in the response header; when None the - TreeID is echoed from *packet*. Use for TREE_CONNECT responses - where the server assigns a new TreeID rather than echoing the - client's value. [MS-SMB2] §3.3.5.7 - :type tree_id: int | None, optional """ resp = smb2.SMB2Packet() # [MS-SMB2] §2.2.1: SMB2_FLAGS_SERVER_TO_REDIR (0x01) on responses @@ -669,6 +573,9 @@ def send_smb2_command( # Server-assigned SessionID - [MS-SMB2] §3.3.5.5.1 resp["SessionID"] = self.smb2_session_id resp["MessageID"] = packet["MessageID"] + # tree_id overrides the echoed TreeID for TREE_CONNECT responses, + # where the server assigns a new TreeID rather than echoing the + # client's value. [MS-SMB2] §3.3.5.7 resp["TreeID"] = tree_id if tree_id is not None else packet["TreeID"] # Real Windows grants 32-256 credits; 1 causes smbclient to exhaust # credits during compound requests (get, dir listing). @@ -681,11 +588,6 @@ def _smb2_error_response(self, packet: smb2.SMB2Packet, status: int) -> None: Per [MS-SMB2] §2.2.2, error responses use the SMB2 ERROR Response structure (StructureSize=0x09) with the appropriate NTSTATUS code. - - :param packet: The original request packet - :type packet: smb2.SMB2Packet - :param status: NTSTATUS error code - :type status: int """ resp = smb2.SMB2Error() self.send_smb2_command(resp.getData(), packet, status=status) @@ -701,11 +603,6 @@ def handle_data(self, data: bytes | None, transport: typing.Any) -> None: 0xFE = SMB2/3 ([MS-SMB2]) and the packet is dispatched to the appropriate command handler via :meth:`handle_smb_packet`. EnableSMB1/EnableSMB2 config gates each path. - - :param data: Initial data from the connection (unused; data is read in the loop) - :type data: bytes | None - :param transport: The transport layer context (unused; kept for interface compatibility) - :type transport: typing.Any """ while True: data = self.recv(8192) @@ -775,13 +672,6 @@ def handle_smb_packet(self, packet: typing.Any, smbv1: bool = False) -> None: Looks up the command opcode in the smb1_commands or smb2_commands dispatch table (populated in :meth:`__init__`) and calls the matching handler method. Unrecognized commands terminate the connection. - - :param packet: Parsed SMB packet (SMB1 or SMB2) from the client - :type packet: typing.Any - :param smbv1: Whether this is an SMB1 packet, defaults to False - :type smbv1: bool, optional - :raises BaseProtoHandler.TerminateConnection: If the command is not - implemented or the handler raises it """ command = packet["Command"] command_name = get_command_name(command, 1 if smbv1 else 2) @@ -829,20 +719,11 @@ def handle_smb_packet(self, packet: typing.Any, smbv1: bool = False) -> None: error_code=nt_errors.STATUS_NOT_IMPLEMENTED, ) - # ══ Phase 1: Negotiate ══════════════════════════════════════════════════════ - - # -- SMB2 Negotiate -- - def _smb3_neg_context_pad(self, data_len: int) -> bytes: """Compute padding bytes for 8-byte alignment of negotiate contexts. [MS-SMB2] §2.2.4: padding between negotiate contexts for 8-byte alignment. Spec does not mandate a pad value; Windows uses 0x00. - - :param data_len: Current data length to pad from - :type data_len: int - :return: Zero-filled padding bytes (0 to 7 bytes) - :rtype: bytes """ return b"\x00" * ((8 - (data_len % 8)) % 8) @@ -854,13 +735,6 @@ def _smb3_build_neg_context_list( Each context is serialized as an :class:`SMB2NegotiateContext` structure followed by padding for 8-byte alignment per [MS-SMB2] §2.2.4. - - :param context_objects: List of ``(context_type, data_bytes)`` tuples - where *context_type* is the negotiate context type ID and - *data_bytes* is the serialized context payload - :type context_objects: list[tuple[int, bytes]] - :return: Concatenated and padded negotiate context list - :rtype: bytes """ context_list = b"" for caps_type, caps in context_objects: @@ -882,11 +756,6 @@ def _smb3_get_target_capabilities( NEGOTIATE request to determine the preferred encryption cipher and signing algorithm. Falls back to AES-128-GCM and AES-CMAC defaults if parsing fails. - - :param request: Parsed SMB2 NEGOTIATE request containing 3.1.1 contexts - :type request: smb2.SMB2Negotiate - :return: Tuple of ``(target_cipher, target_sign)`` algorithm IDs - :rtype: tuple[int, ...] """ target_cipher = smb3.SMB2_ENCRYPTION_AES128_GCM target_sign = 0x001 # [MS-SMB2] §2.2.3.1.7: AES-CMAC signing algorithm @@ -928,14 +797,6 @@ def _build_smb2_negotiate_response( realistic max sizes for direct TCP, a SPNEGO security token, and (for SMB 3.1.1) negotiate contexts for preauth integrity, encryption, and signing algorithms. - - :param target_revision: The selected SMB2 dialect hex constant - :type target_revision: int - :param request: The client's parsed NEGOTIATE request, used to extract - 3.1.1 negotiate contexts, defaults to None - :type request: smb2.SMB2Negotiate | None, optional - :return: The populated SMB2 NEGOTIATE response structure - :rtype: smb2.SMB2Negotiate_Response """ command = smb2.SMB2Negotiate_Response() # [MS-SMB2] §2.2.4 / §3.3.5.4: SMB2_NEGOTIATE_SIGNING_ENABLED MUST be set @@ -1027,11 +888,6 @@ def handle_smb2_negotiate(self, packet: smb2.SMB2Packet) -> None: If no common dialect exists, responds with STATUS_NOT_SUPPORTED. Spec: [MS-SMB2] §3.3.5.4 - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet - :raises BaseProtoHandler.TerminateConnection: If the client sends no - dialects or no common dialect is available """ req = smb3.SMB2Negotiate(data=packet["Data"]) dialect_count: int = req["DialectCount"] @@ -1150,8 +1006,6 @@ def handle_smb2_negotiate(self, packet: smb2.SMB2Packet) -> None: self.send_smb2_command(command.getData()) - # -- SMB1 Negotiate -- - def _fill_smb1_negotiate_params( self, params: typing.Any, nt_lm_index: int, server_time: int ) -> None: @@ -1179,12 +1033,6 @@ def handle_smb1_negotiate(self, packet: smb.NewSMBPacket) -> None: (when AllowSMB1Upgrade is enabled and SMB2 dialect strings are present), SMB1 extended security (NTLMSSP/SPNEGO), and SMB1 non-extended security (raw challenge/response or plaintext). - - :param packet: Parsed SMB1 packet from the client - :type packet: smb.NewSMBPacket - :raises BaseProtoHandler.TerminateConnection: If the client sends no - dialects or does not offer NT LM 0.12 (and SMB2 upgrade is - not available) """ resp = smb.NewSMBPacket() resp["Flags1"] = smb.SMB.FLAGS1_REPLY @@ -1269,7 +1117,6 @@ def handle_smb1_negotiate(self, packet: smb.NewSMBPacket) -> None: use_extended = bool(packet["Flags2"] & smb.SMB.FLAGS2_EXTENDED_SECURITY) if use_extended: - # --- Extended security path (NTLMSSP/SPNEGO) --- self.smb1_extended_security = True # [MS-SMB] §2.2.3.1: response Flags2 for extended security negotiate @@ -1294,7 +1141,6 @@ def handle_smb1_negotiate(self, packet: smb.NewSMBPacket) -> None: _dialects_parameters["ChallengeLength"] = 0 dialect_label = "NT LM 0.12" else: - # --- Non-extended security path (raw challenge/response) --- # [MS-SMB] §2.2.4.5.2.2 self.smb1_extended_security = False self.smb1_challenge = self.config.ntlm_challenge @@ -1337,10 +1183,6 @@ def handle_smb1_negotiate(self, packet: smb.NewSMBPacket) -> None: resp.addCommand(command) self.send_data(resp.getData()) - # ══ Phase 2: Authentication ═══════════════════════════════════════════════════ - - # -- NTLMSSP (shared SMB1/SMB2) -- - def handle_ntlmssp( self, token: bytes, @@ -1362,18 +1204,6 @@ def handle_ntlmssp( AUTHENTICATE_MESSAGE, returns either STATUS_ACCOUNT_DISABLED (to trigger a retry with different credentials) or the final configured error code. - - :param token: Raw security token from the SMB session setup request - :type token: bytes - :param command_name: SMB command name for log attribution, defaults to - "SMB2_SESSION_SETUP" - :type command_name: str, optional - :raises BaseProtoHandler.TerminateConnection: If the GSSAPI token is - malformed, the NTLM token length is invalid, an unsupported - NTLM message type is received, or a CHALLENGE_MESSAGE arrives - unexpectedly - :return: Tuple of (response_token_bytes, ntstatus_error_code) - :rtype: tuple[bytes, int] """ is_gssapi = not token.startswith(b"NTLMSSP") @@ -1540,11 +1370,6 @@ def _resolve_auth_error_code(self) -> int: When ``ErrorCode`` has been confiured to a valid value, it will be returned regardless of whether ``CapturesPerConnection`` is set. - - :return: NTSTATUS code -- STATUS_ACCOUNT_DISABLED for intermediate - attempts, or STATUS_SUCCESS for the final attempt (to allow - tree connect path capture) - :rtype: int """ self.auth_attempt_count += 1 max_captures = self.smb_config.smb_captures_per_connection @@ -1575,8 +1400,6 @@ def _resolve_auth_error_code(self) -> int: ) return error_code - # -- SMB2 Session -- - def handle_smb2_session_setup(self, packet: smb2.SMB2Packet) -> None: """Handle an SMB2 SESSION_SETUP request. @@ -1587,9 +1410,6 @@ def handle_smb2_session_setup(self, packet: smb2.SMB2Packet) -> None: while the exchange is in progress, or the final error/success code). Spec: [MS-SMB2] §3.3.5.5 - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ req = smb2.SMB2SessionSetup(data=packet["Data"]) @@ -1666,9 +1486,6 @@ def handle_smb2_logoff(self, packet: smb2.SMB2Packet) -> None: Logs the client logoff, resets the authenticated flag, and sends a successful LOGOFF response. - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ self.logger.debug("SMB2_LOGOFF", is_client=True) @@ -1680,19 +1497,12 @@ def handle_smb2_logoff(self, packet: smb2.SMB2Packet) -> None: status=nt_errors.STATUS_SUCCESS, ) - # -- SMB1 Session -- - def handle_smb1_session_setup(self, packet: smb.NewSMBPacket) -> None: """Handle SMB1 SESSION_SETUP_ANDX -- [MS-SMB] §3.3.5.3. Dispatches to extended security (WordCount=12, NTLMSSP/SPNEGO via :meth:`handle_ntlmssp`) or basic security (WordCount=13, raw challenge/response via :meth:`handle_smb1_session_setup_basic`). - - :param packet: Parsed SMB1 packet from the client - :type packet: smb.NewSMBPacket - :raises BaseProtoHandler.TerminateConnection: If the WordCount is - neither 12 nor 13 """ command = smb.SMBCommand(packet["Data"][0]) # [MS-SMB] §2.2.4.6.1: WordCount == 0x0C for extended security @@ -1796,12 +1606,6 @@ def handle_smb1_session_setup_basic( Spec: [MS-CIFS] §2.2.4.53.1 (request), §2.2.4.53.2 (response), §3.2.4.2.4 (plaintext-despite-challenge) - - :param packet: The original SMB1 packet from the client - :type packet: smb.NewSMBPacket - :param command: The parsed SMB command containing session setup parameters - and data fields (WordCount=13) - :type command: smb.SMBCommand """ cfg = self.smb_config setup_params = smb.SMBSessionSetupAndX_Parameters(command["Parameters"]) @@ -2008,9 +1812,6 @@ def handle_smb1_logoff(self, packet: smb.NewSMBPacket) -> None: Sends a proper LOGOFF response (AndX parameters only, no data) and terminates the connection. - - :param packet: Parsed SMB1 packet from the client - :type packet: smb.NewSMBPacket """ self.logger.debug("SMB_COM_LOGOFF_ANDX", is_client=True) @@ -2024,20 +1825,11 @@ def handle_smb1_logoff(self, packet: smb.NewSMBPacket) -> None: ) raise BaseProtoHandler.TerminateConnection - # ══ Phase 3: Tree Connect ═══════════════════════════════════════════════════ - - # -- SMB2 Tree -- - def _extract_smb2_tree_path(self, packet: smb2.SMB2Packet) -> str: r"""Extract the UNC path from an SMB2 TREE_CONNECT request. Uses the raw ``PathOffset``/``PathLength`` fields from the wire rather than impacket's ``Buffer`` field (which has alignment issues). - - :param packet: Parsed SMB2 packet - :type packet: smb2.SMB2Packet - :return: Decoded UNC path (e.g. ``\\\\10.0.0.50\\share``) - :rtype: str """ req = smb2.SMB2TreeConnect(data=packet["Data"]) raw_data: bytes = packet["Data"] @@ -2064,9 +1856,6 @@ def handle_smb2_tree_connect(self, packet: smb2.SMB2Packet) -> None: client proceeds to CREATE / READ / CLOSE, allowing filename capture. The share path (e.g. ``\\\\10.0.0.50\\share``) is recorded in :attr:`client_info`. - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ path = "" try: @@ -2120,9 +1909,6 @@ def handle_smb2_tree_disconnect(self, packet: smb2.SMB2Packet) -> None: Acknowledges tree disconnect requests. Per [MS-SMB2] §2.2.12, the response is a 4-byte structure with only StructureSize and Reserved. - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ self.logger.debug( "SMB2_TREE_DISCONNECT TreeId=%d", packet["TreeID"], is_client=True @@ -2130,8 +1916,6 @@ def handle_smb2_tree_disconnect(self, packet: smb2.SMB2Packet) -> None: resp = smb2.SMB2TreeDisconnect_Response() self.send_smb2_command(resp.getData(), packet) - # -- SMB2 IOCTL -- - def handle_smb2_ioctl(self, packet: smb2.SMB2Packet) -> None: """SMB2 IOCTL handler -- [MS-SMB2] §3.3.5.15. @@ -2146,9 +1930,6 @@ def handle_smb2_ioctl(self, packet: smb2.SMB2Packet) -> None: non-DFS servers MUST return ``STATUS_FS_DRIVER_REQUIRED``. All other codes return ``STATUS_FS_DRIVER_REQUIRED``. - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ ctl_code = 0 try: @@ -2175,11 +1956,6 @@ def _handle_validate_negotiate( The client sends its view of the negotiated parameters. The server responds with its own values so the client can verify they match. If they don't, the client drops the connection (anti-downgrade). - - :param packet: Parsed SMB2 packet - :type packet: smb2.SMB2Packet - :param req: Parsed IOCTL request - :type req: smb2.SMB2Ioctl """ try: vni = smb2.VALIDATE_NEGOTIATE_INFO(req["Buffer"]) @@ -2221,8 +1997,6 @@ def _handle_validate_negotiate( self.logger.debug("FSCTL_VALIDATE_NEGOTIATE_INFO failed", exc_info=True) self._smb2_error_response(packet, nt_errors.STATUS_ACCESS_DENIED) - # -- SMB1 Tree -- - def handle_smb1_tree_connect(self, packet: smb.NewSMBPacket) -> None: r"""SMB1 TREE_CONNECT_ANDX handler -- [MS-CIFS] §2.2.4.55. @@ -2232,9 +2006,6 @@ def handle_smb1_tree_connect(self, packet: smb.NewSMBPacket) -> None: - **Non-IPC$**: accepted so the client proceeds to NT_CREATE / READ, allowing filename capture. The share path is recorded in :attr:`client_info`. - - :param packet: Parsed SMB1 packet from the client - :type packet: smb.NewSMBPacket """ path = "" try: @@ -2329,9 +2100,6 @@ def handle_smb1_tree_disconnect(self, packet: smb.NewSMBPacket) -> None: Acknowledges tree disconnect requests. ``SMB_COM_TREE_DISCONNECT`` is NOT an AndX command - the response has zero parameter words and zero data bytes. - - :param packet: Parsed SMB1 packet from the client - :type packet: smb.NewSMBPacket """ self.logger.debug("SMB_COM_TREE_DISCONNECT Tid=%d", packet["Tid"], is_client=True) self.send_smb1_command( @@ -2341,10 +2109,6 @@ def handle_smb1_tree_disconnect(self, packet: smb.NewSMBPacket) -> None: packet, ) - # ══ Phase 4: File Operations ══════════════════════════════════════════════════ - - # -- SMB2 File Operations -- - def handle_smb2_create(self, packet: smb2.SMB2Packet) -> None: """SMB2 CREATE handler -- [MS-SMB2] §3.3.5.9. @@ -2354,9 +2118,6 @@ def handle_smb2_create(self, packet: smb2.SMB2Packet) -> None: set to the current server time. Empty names (directory opens) get ``FILE_ATTRIBUTE_DIRECTORY``; all others get ``FILE_ATTRIBUTE_NORMAL``. - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ name = "" try: @@ -2418,9 +2179,6 @@ def handle_smb2_query_directory(self, packet: smb2.SMB2Packet) -> None: Returns ``STATUS_NO_MORE_FILES`` for all directory queries. The fake directories are empty, so enumeration returns no entries immediately. - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ try: req = smb2.SMB2QueryDirectory(packet["Data"]) @@ -2449,9 +2207,6 @@ def handle_smb2_query_info(self, packet: smb2.SMB2Packet) -> None: Sent by Win7, Win8.1, Srv2008, Srv2008R2, Srv2012R2. - **FileStandardInfo** (5): size + link count + directory flag. Sent by Srv2008. - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ info_type = 0 file_info_class = 0 @@ -2604,9 +2359,6 @@ def handle_smb2_read(self, packet: smb2.SMB2Packet) -> None: Returns ``STATUS_END_OF_FILE`` for all read requests. The fake files created by :meth:`handle_smb2_create` have zero size, so any read attempt hits EOF immediately. - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ try: req = smb2.SMB2Read(packet["Data"]) @@ -2627,9 +2379,6 @@ def handle_smb2_close(self, packet: smb2.SMB2Packet) -> None: Acknowledges close requests with a spec-compliant CLOSE response. Per [MS-SMB2] §2.2.16, StructureSize MUST be 0x3C (60). - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ self.logger.debug("SMB2_CLOSE", is_client=True) # SMB2Close_Response has all zeros for timestamps/sizes - spec-compliant @@ -2642,9 +2391,6 @@ def handle_smb2_write(self, packet: smb2.SMB2Packet) -> None: Acknowledges write requests. No data is actually written - the fake files are read-only scaffolding. Returns the requested byte count as ``Count`` so the client believes the write succeeded. - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ count = 0 try: @@ -2665,9 +2411,6 @@ def handle_smb2_flush(self, packet: smb2.SMB2Packet) -> None: Acknowledges flush requests. No data is actually flushed - the fake files have no backing store. Observed from Win8.1 and Srv2012R2 (SMB 3.0.2 IS_GUEST clients) after WRITE operations. - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ self.logger.debug("SMB2_FLUSH", is_client=True) resp = smb2.SMB2Flush_Response() @@ -2678,9 +2421,6 @@ def handle_smb2_lock(self, packet: smb2.SMB2Packet) -> None: Acknowledges lock requests. Response is 4 bytes (StructureSize + Reserved). - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ self.logger.debug("SMB2_LOCK", is_client=True) resp = smb2.SMB2Lock_Response() @@ -2692,9 +2432,6 @@ def handle_smb2_set_info(self, packet: smb2.SMB2Packet) -> None: Acknowledges set-info requests. No attributes are actually changed - the fake files are immutable scaffolding. Response is 2 bytes (StructureSize only). - - :param packet: Parsed SMB2 packet from the client - :type packet: smb2.SMB2Packet """ try: req = smb2.SMB2SetInfo(packet["Data"]) @@ -2709,8 +2446,6 @@ def handle_smb2_set_info(self, packet: smb2.SMB2Packet) -> None: resp = smb2.SMB2SetInfo_Response() self.send_smb2_command(resp.getData(), packet) - # -- SMB1 File Operations -- - def handle_smb1_nt_create(self, packet: smb.NewSMBPacket) -> None: """SMB1 NT_CREATE_ANDX handler -- [MS-SMB] §3.3.5.6. @@ -2718,9 +2453,6 @@ def handle_smb1_nt_create(self, packet: smb.NewSMBPacket) -> None: to READ_ANDX, allowing filename capture. Empty filenames (share root opens) get ``FILE_ATTRIBUTE_DIRECTORY``; all others get ``FILE_ATTRIBUTE_NORMAL``. - - :param packet: Parsed SMB1 packet from the client - :type packet: smb.NewSMBPacket """ name = "" try: @@ -2799,11 +2531,6 @@ def _send_smb1_trans2_response( SMB data section with absolute offsets from the SMB header start. Layout: SMBheader(32) + WordCount(1) + Words(20) + ByteCount(2) = 55 bytes fixed. Pad1(1) aligns trans_parameters to offset 56. - - :param packet: The original TRANS2 request - :param trans_parameters: Subcommand-specific parameter bytes - :param trans_data: Subcommand-specific data bytes - :param error_code: NTSTATUS error code, or None for STATUS_SUCCESS """ # Absolute offsets from SMB header start # 32(hdr) + 1(WC) + 20(Words) + 2(BC) = 55 @@ -2853,11 +2580,6 @@ def _build_trans2_file_info(self, info_level: int) -> bytes | None: # noqa: PLR 3. **Raw FileInformationClass** (small numbers 3-38): observed from XP SP3 in pcap -- sends the class number directly without the 0x03E8 base. Handled by the same native-class dispatch. - - :param info_level: The InformationLevel from the TRANS2 request - :type info_level: int - :return: Serialized file info bytes, or None if unsupported - :rtype: bytes | None """ now = get_server_time() @@ -3010,9 +2732,6 @@ def handle_smb1_trans2(self, packet: smb.NewSMBPacket) -> None: ``0x03ed`` (FileStandardInformation) after NT_CREATE_ANDX. - ``TRANS2_FIND_FIRST2`` (0x0001): ``STATUS_NO_MORE_FILES`` - Others: ``STATUS_NOT_IMPLEMENTED`` - - :param packet: Parsed SMB1 packet from the client - :type packet: smb.NewSMBPacket """ subcommand = -1 try: @@ -3121,9 +2840,6 @@ def handle_smb1_read(self, packet: smb.NewSMBPacket) -> None: Returns ``STATUS_END_OF_FILE`` for all read requests. The fake files have zero size, so any read hits EOF immediately. - - :param packet: Parsed SMB1 packet from the client - :type packet: smb.NewSMBPacket """ try: cmd = smb.SMBCommand(packet["Data"][0]) @@ -3150,9 +2866,6 @@ def handle_smb1_close(self, packet: smb.NewSMBPacket) -> None: Acknowledges close requests. ``SMB_COM_CLOSE`` is NOT an AndX command - the response has zero parameter words and zero data bytes. - - :param packet: Parsed SMB1 packet from the client - :type packet: smb.NewSMBPacket """ try: cmd = smb.SMBCommand(packet["Data"][0]) @@ -3168,7 +2881,6 @@ def handle_smb1_close(self, packet: smb.NewSMBPacket) -> None: ) -# --- Server ------------------------------------------------------------------ class SMBServer(ThreadingTCPServer): """Threaded TCP server that spawns an :class:`SMBHandler` per connection. @@ -3192,16 +2904,6 @@ def __init__( Generates a random 16-byte ServerGuid per [MS-SMB2] §2.2.4 that persists for the lifetime of this server instance. Delegates to :class:`ThreadingTCPServer` for socket binding and thread management. - - :param config: The active session configuration - :type config: SessionConfig - :param server_config: SMB-specific server configuration from TOML - :type server_config: SMBServerConfig - :param server_address: The ``(bind_address, port)`` tuple, defaults to None - :type server_address: tuple[str, int] | None, optional - :param RequestHandlerClass: Override handler class, defaults to None - (uses :class:`SMBHandler`) - :type RequestHandlerClass: type | None, optional """ self.server_config = server_config # Stable ServerGuid per server instance - [MS-SMB2] §2.2.4 @@ -3215,11 +2917,6 @@ def finish_request( Overrides :meth:`ThreadingTCPServer.finish_request` to pass the additional ``server_config`` argument required by :class:`SMBHandler`. - - :param request: The raw socket/request object for this connection - :type request: typing.Any - :param client_address: The ``(host, port)`` tuple of the connecting client - :type client_address: tuple[str, int] """ typing.cast("type", self.RequestHandlerClass)( self.config, self.server_config, request, client_address, self diff --git a/dementor/protocols/ssdp.py b/dementor/protocols/ssdp.py index f869bbc..3cbda6d 100644 --- a/dementor/protocols/ssdp.py +++ b/dementor/protocols/ssdp.py @@ -232,7 +232,7 @@ def handle_advertisement(self) -> None: self.logger.display(f"(Notify: [i]alive[/i]) {self.describe_device(usn)}") # [1.2.3 Device unavailable -- NOTIFY with ssdp:byebye] - case "ssdp::byebye": + case "ssdp:byebye": usn = UDN(self.message["USN"] or "uuid:") self.logger.display( f"(Notify: [i]byebye[/i]) {self.describe_device(usn)}" @@ -259,7 +259,7 @@ def handle_search(self, transport) -> None: if len(tokens) == 2: search_tyoe = "UUID" target_text = f"[i]{escape(tokens[1])}[/i]" - elif len(tokens) >= 3: + elif len(tokens) >= 4: search_tyoe = escape(tokens[2]) target_text = f"[i]{escape(tokens[1])}[/i]/[b]{escape(tokens[3])}[/b]" else: diff --git a/dementor/tui/action.py b/dementor/tui/action.py index 93ffe32..d9d3447 100644 --- a/dementor/tui/action.py +++ b/dementor/tui/action.py @@ -18,30 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # pyright: basic -"""Top-level utilities for defining REPL actions. - -This module provides an abstract base class :class:`ReplAction` that all -interactive commands for the :class:`~dementor.tui.repl.Repl` must inherit from. -It also supplies the :func:`command` decorator used to register concrete -action classes in the global :data:`REPL_COMMANDS` registry. - -The design mirrors the command-pattern used throughout the project and enables -dynamic discovery of available commands at runtime. - -Typical usage:: - - from dementor.tui.action import ReplAction, command - - - @command - class MyCommand(ReplAction): - '''Implementation of a custom REPL command.''' - - def execute(self, argv): ... - -The :func:`command` decorator can also be used with an explicit ``names`` -attribute on the class to provide aliases. -""" +"""REPL action registration utilities.""" import argparse @@ -60,14 +37,6 @@ def execute(self, argv): ... class ReplAction(ABC): - """Abstract base class for all REPL actions. - - Sub-classes represent a single command that can be invoked from the - interactive REPL. Each concrete action must implement :meth:`execute` - and may optionally provide a custom :class:`argparse.ArgumentParser` - via :meth:`get_parser`. - """ - names: list[str] def __init__(self, repl: "Repl") -> None: @@ -75,43 +44,17 @@ def __init__(self, repl: "Repl") -> None: self.repl: Repl = repl def get_parser(self) -> argparse.ArgumentParser | None: - """Return an argument parser for the command. - - Sub-classes may override this method to expose command-line options - that are parsed before :meth:`execute` is called. The default - implementation returns ``None`` which signals that the command does - not accept any additional arguments. - """ return None @abstractmethod def execute(self, argv: argparse.Namespace) -> None: - """Execute the command logic. - - Concrete subclasses must implement this method. The *argv* argument - contains the parsed result of the parser returned by - :meth:`get_parser`. If :meth:`get_parser` returns ``None`` the *argv* - will be an empty :class:`argparse.Namespace`. - """ + pass def get_completions(self, word: str, document: Document) -> list[str]: - """Return a list of completion strings for the given *word*. - - Sub-classes may override this to provide custom completions. - The default implementation returns an empty list. - """ return [] def command(cls: type[_AT]) -> type[_AT]: - """Class decorator that registers a REPL action. - - The decorator inspects the ``names`` attribute of the class. If the - attribute is missing or empty, the class name is used as the command key. - Otherwise each entry in ``names`` is treated as an alias. All keys are - stored in the global :data:`REPL_COMMANDS` dictionary which maps a command - string to the implementing class. - """ names: list[str] | None = getattr(cls, "names", None) if not names: REPL_COMMANDS[cls.__name__] = cls diff --git a/dementor/tui/commands/proto.py b/dementor/tui/commands/proto.py index b2b0d4c..2c37563 100644 --- a/dementor/tui/commands/proto.py +++ b/dementor/tui/commands/proto.py @@ -45,27 +45,11 @@ @command class ServiceCommand(ReplAction): - """Command implementation for managing protocol services. - - This class is registered under the name proto and provides the - sub-commands on/start, off/stop and status. The - status command can be invoked with a specific protocol name to show a - detailed view or without a name to list all services. - """ + """Command implementation for managing protocol services.""" names: list[str] = ["proto"] def get_proto_parser(self) -> argparse.ArgumentParser: - """Create an :class:`argparse.ArgumentParser` for the proto command. - - The parser description contains a comma-separated list of currently - available protocol names. Sub-commands are attached a fn attribute - that points to the concrete method handling the action; this attribute - is later used by :meth:`execute`. - - :return: Configured argument parser. - :rtype: argparse.ArgumentParser - """ # Available protocol names (lower-case) for the help text names = [name.lower() for name in self.repl.session.manager.threads] parser = argparse.ArgumentParser( @@ -125,15 +109,6 @@ def get_proto_parser(self) -> argparse.ArgumentParser: @override def execute(self, argv: argparse.Namespace) -> None: - """Entry point invoked by the REPL for the proto command. - - argv.args_raw holds the arguments following the proto keyword. - When no protocol name is supplied an error message is shown. Supplying - status without additional arguments triggers a full service list. - - :param argv: Parsed arguments from the REPL. - :type argv: argparse.Namespace - """ args_raw: list[str] = list(argv.args_raw) if not args_raw: self.repl.console.print("[bold red]Service name must be specified[/]") @@ -155,15 +130,6 @@ def execute(self, argv: argparse.Namespace) -> None: parsed.fn(name, parsed) def service_stop(self, name: str, argv: argparse.Namespace) -> None: - """Stop a running protocol service. - - Prompts the user for confirmation unless the --yes flag is set. - - :param name: Protocol name (case-insensitive). - :type name: str - :param argv: Parsed arguments; argv.yes bypasses the prompt. - :type argv: argparse.Namespace - """ manager: ProtocolManager = self.repl.session.manager if not manager.is_running(name): self.repl.console.print(f"[bold yellow]No servers running for {name}![/]") @@ -195,19 +161,6 @@ def service_stop(self, name: str, argv: argparse.Namespace) -> None: def service_status( self, name: str, argv: argparse.Namespace | None = None, details: bool = True ) -> None: - """Print the status of a single protocol service. - - When details is True a tree view with address/port information - for each thread is rendered; otherwise a compact ON/OFF line is - printed. - - :param name: Protocol name. - :type name: str - :param argv: Unused placeholder for compatibility with the parser. - :type argv: argparse.Namespace | None - :param details: Whether to show a detailed tree view. - :type details: bool - """ console: Console = self.repl.console manager: ProtocolManager = self.repl.session.manager protocol = manager.protocols[name.lower()] @@ -239,11 +192,6 @@ def service_status( console.print(tree) def service_reload(self, name: str, argv: argparse.Namespace) -> None: - """Reload a protocol service. - - Stops the service if running, reloads the protocol module via the loader, - recreates threads and starts the service again. - """ manager: ProtocolManager = self.repl.session.manager status = self.repl.console.status( f"[bold red]Reloading...[/bold red] ([dim]{name}[/])", @@ -256,19 +204,15 @@ def service_reload(self, name: str, argv: argparse.Namespace) -> None: status.start() try: - # Stop if running if manager.is_running(name): status.update(f"[bold red]Stopping services...[/] ([dim]{name}[/])") manager.stop(name) # Reload protocol module - # Resolve protocol path and reload using loader protocol_path = manager.session.protocols.get(name.lower()) if protocol_path: status.update(f"[bold red]Reloading module...[/] ([dim]{name}[/])") - # Load fresh module and replace in manager.loader new_module = manager.loader.load_protocol(protocol_path) # Recreate protocol instance - # Find protocol class name via __proto__ list proto_names = getattr(new_module, "__proto__", []) for proto_name in proto_names: proto_ty = getattr(new_module, proto_name, None) @@ -293,15 +237,6 @@ def service_reload(self, name: str, argv: argparse.Namespace) -> None: status.stop() def service_start(self, name: str, argv: argparse.Namespace) -> None: - """Start a protocol service if it is not already running. - - A spinner is shown while the service threads are created and started. - - :param name: Protocol name. - :type name: str - :param argv: Parsed arguments (currently unused). - :type argv: argparse.Namespace - """ manager: ProtocolManager = self.repl.session.manager if manager.is_running(name): self.repl.console.print( @@ -324,7 +259,6 @@ def service_start(self, name: str, argv: argparse.Namespace) -> None: status.stop() def _list_fields(self, cfg: TomlConfig) -> None: - """List configurable fields of a TomlConfig instance.""" fields = [f"[b]{attr.qname}[/]: {attr.attr_name}" for attr in cfg._fields_] console = self.repl.console console.print( @@ -340,10 +274,6 @@ def _list_fields(self, cfg: TomlConfig) -> None: ) def service_config(self, name: str, argv: argparse.Namespace) -> None: - """Inspect or modify runtime server configuration for a protocol. - - Only fields defined in the protocol's ``_fields_`` are editable. - """ manager: ProtocolManager = self.repl.session.manager console = self.repl.console threads = manager.threads.get(name.lower(), []) @@ -379,7 +309,6 @@ def service_config(self, name: str, argv: argparse.Namespace) -> None: "[b red]Nested keys are not supported. Use a top-level field name.[/]" ) return - # Parse key/value if "=" in key_path: key, raw_value = key_path.split("=", 1) else: @@ -388,7 +317,6 @@ def service_config(self, name: str, argv: argparse.Namespace) -> None: cfg_targets = ( [cfg] if not apply_all else [thread.server_config for thread in threads] ) - # Resolve field (case-insensitive) from _fields_ for i, cfg in enumerate(cfg_targets): cfg_fields: list[Attribute] = cfg._fields_ target_key: str = key.lower() @@ -438,12 +366,6 @@ def convert(val: Any) -> Any: ) def service_status_all(self) -> None: - """Display the status of **all** configured protocol services. - - Services are listed alphabetically, with poisoner protocols highlighted - separately. - - """ console: Console = self.repl.console manager: ProtocolManager = self.repl.session.manager @@ -464,13 +386,6 @@ def service_status_all(self) -> None: @override def get_completions(self, word: str, document: Document) -> list[str]: - """Provide completions for protocol name, sub-commands and flags. - - * Token 0 - the command name ``proto`` (already supplied by the REPL). - * Token 1 - the protocol service name. - * Token 2 - the sub-command (on/off/status/config/reload). - * Tokens 3+ - flags/options specific to the chosen sub-command. - """ # Split the current line, handling simple quoting. try: tokens = shlex.split(document.text_before_cursor) diff --git a/dementor/tui/completer.py b/dementor/tui/completer.py index c2b805c..a11e0b5 100644 --- a/dementor/tui/completer.py +++ b/dementor/tui/completer.py @@ -41,38 +41,18 @@ class ReplCompleter(Completer): 1. **Command completion** - when the cursor is at the first word of the line, it suggests all registered command names from :data:`dementor.tui.action.REPL_COMMANDS`. - 2. **Flag completion** - after a command has been entered, the completer - inspects the command's ``argparse`` parser (if any) and offers the - defined option strings (e.g. ``--interface``) as completions. - - The goal is to provide a runtime, introspection-based completion experience - without requiring a static completion file. + 2. **Argument completion** - after a command has been entered, the + completer delegates to that command's own + :meth:`~dementor.tui.action.ReplAction.get_completions` hook. """ def __init__(self, repl: "Repl") -> None: self.repl: Repl = repl - # Helper ----------------------------------------------------------------- def _iter_command_names(self) -> Iterable[str]: """Yield all command names/aliases registered in ``REPL_COMMANDS``.""" yield from REPL_COMMANDS.keys() - def _get_parser_for_command(self, command_name: str): - """Return the ``argparse.ArgumentParser`` for *command_name* or ``None``. - - The function lazily creates an instance of the action class associated - with *command_name* and calls its ``get_parser`` method. - """ - action_cls = REPL_COMMANDS.get(command_name) - if not action_cls: - return None - try: - action_obj = action_cls(self.repl) - return action_obj.get_parser() - except Exception: - # Guard against actions that require additional runtime state. - return None - # Completer interface ------------------------------------------------------ @override def get_completions( @@ -85,25 +65,21 @@ def get_completions( quoting. """ text_before = document.text_before_cursor.lstrip() - # Determine the current word to replace. word = document.get_word_before_cursor(WORD=True) - # Split the line into tokens - ``shlex`` is used for proper handling of - # quoted arguments but we fall back to a simple split if parsing fails. + # shlex handles quoted arguments correctly; fall back to a plain + # split if the line has unbalanced quotes (still being typed). try: tokens = shlex.split(text_before) except Exception: tokens = text_before.split() - # No tokens yet -> suggest command names. if not tokens: for name in self._iter_command_names(): if name.startswith(word): yield Completion(name, start_position=-len(word)) return - # First token is the command. command = tokens[0] - # After the command - collect completions from the command's hook. completions: set[str] = set() action_cls = REPL_COMMANDS.get(command) if action_cls: @@ -118,7 +94,6 @@ def get_completions( if name.startswith(word): yield Completion(name, start_position=-len(word)) - # Yield matching completions for opt in sorted(completions): if opt.startswith(word): yield Completion(opt, start_position=-len(word)) diff --git a/dementor/tui/repl.py b/dementor/tui/repl.py index 03de1a8..e6c8b9e 100644 --- a/dementor/tui/repl.py +++ b/dementor/tui/repl.py @@ -17,6 +17,8 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +"""Interactive command dispatcher.""" + import sqlalchemy import argparse import shlex @@ -34,42 +36,10 @@ class Repl: - """Main REPL class. - - The REPL (Read-Eval-Print Loop) provides an interactive command-line - interface for Dementor. It is built on top of :mod:`prompt_toolkit` for - advanced line editing, history and asynchronous input, and :mod:`rich` for - coloured output. The loop displays a dynamic prompt that shows the - application version, the currently selected network interface, the active - database backend and the number of stored credentials. If debug mode is - enabled a ``[Debug]`` flag is added. - - The REPL integrates tightly with :class:`~dementor.config.session.SessionConfig`: - * ``session.db`` gives access to the SQLAlchemy engine and model objects. - * ``session.loop`` is the asyncio event loop used to run the asynchronous - ``arun`` coroutine. - * ``session.interface`` and ``session.debug`` influence the prompt - appearance. - - The class is deliberately lightweight - it only orchestrates input, - builds the prompt and delegates all functional work to the action classes. - This makes it easy to extend the CLI by adding new entries to - ``action.REPL_COMMANDS`` without modifying the REPL core. - - :param session: The current session configuration providing access to the - database, event loop and other runtime options. - :type session: SessionConfig - """ - def __init__( self, session: SessionConfig, ) -> None: - """Create a new REPL instance. - - :param session: The active session configuration. - :type session: SessionConfig - """ self.session: SessionConfig = session self.prompt_session: PromptSession[str] = PromptSession( completer=ReplCompleter(self) @@ -77,11 +47,6 @@ def __init__( self.console: Console = Console() def get_prompt(self) -> list[tuple[str, str]]: - """Build the prompt parts for the REPL. - - :return: A list of style/segment tuples understood by ``prompt_toolkit``. - :rtype: list[tuple[str, str]] - """ parts: list[tuple[str, str]] = [] parts.append(("bold", "dm")) @@ -103,10 +68,6 @@ def get_prompt(self) -> list[tuple[str, str]]: return parts def run(self) -> None: - """Run the REPL synchronously. - - This method starts the asyncio event loop and executes :meth:`arun`. - """ self.session.loop.run_until_complete(self.arun()) def get_placeholder(self) -> list[tuple[str, str]]: @@ -118,11 +79,6 @@ def get_placeholder(self) -> list[tuple[str, str]]: ] async def arun(self) -> None: - """Main asynchronous REPL loop. - - It continuously reads user input, handles interruptions and routes - commands to the appropriate action classes. - """ with patch_stdout(raw=True): while True: try: @@ -148,11 +104,6 @@ async def arun(self) -> None: ) def _handle_line(self, line: str) -> None: - """Parse and dispatch a single line of user input. - - :param line: The raw command line entered by the user. - :type line: str - """ line = line.strip() if not line: return diff --git a/docs/source/compat.rst b/docs/source/compat.rst index 09cb305..b2e8cdd 100644 --- a/docs/source/compat.rst +++ b/docs/source/compat.rst @@ -27,7 +27,7 @@ in development. The legend for each symbol is as follows: Supported Protocols Responder (3.2.2.0) - Dementor (1.0.0.dev24) + Dementor (1.0.0.dev25) diff --git a/docs/source/conf.py b/docs/source/conf.py index fa11717..0a33bbc 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,7 +9,7 @@ project = "Dementor" copyright = "2025-Present, MatrixEditor" author = "MatrixEditor" -release = "1.0.0.dev24" +release = "1.0.0.dev25" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/source/config/logging.rst b/docs/source/config/logging.rst index 157cc89..4b68006 100644 --- a/docs/source/config/logging.rst +++ b/docs/source/config/logging.rst @@ -20,6 +20,9 @@ Section ``[Log]`` format ``log_%Y-%m-%d-%H-%M-%S.log`` and cannot be customized. This setting does **not** affect terminal output. + .. versionchanged:: v1.0.0.dev25 + Logging is now enabled by default. + .. py:attribute:: LogDir :type: RelativePath | RelativeWorkspacePath | AbsolutePath :value: "logs" diff --git a/pyproject.toml b/pyproject.toml index e44b864..fa846c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "dementor" -version = "1.0.0.dev24" +version = "1.0.0.dev25" # REVISIT: dynamic version # dynamic = ["version"] description = "LLMNR/NBT-NS/mDNS Poisoner and rogue service provider" diff --git a/tests/test_filters.py b/tests/test_filters.py index a984c5f..d4ede38 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -107,36 +107,12 @@ def test_literal_pattern_is_none(self): class TestFilterObjGlob: - @pytest.mark.skipif( - (sys.version_info.major, sys.version_info.minor) < (3, 13), - reason="glob.translate requires Python 3.13+", - ) def test_glob_wildcard(self): f = FilterObj("g:*.example.com") assert f.matches("api.example.com") is True assert f.matches("www.example.com") is True assert f.matches("evil.net") is False - @pytest.mark.skipif( - (sys.version_info.major, sys.version_info.minor) >= (3, 13), - reason="This test covers the <3.13 fallback path only", - ) - def test_glob_pre_313_falls_back_to_literal(self): - with pytest.warns(UserWarning, match="glob.translate"): - f = FilterObj("g:*.example.com") - # In fallback mode, pattern is None and matches uses exact string compare - assert f.pattern is None - # Target has the prefix stripped - assert f.target == "*.example.com" - - def test_glob_target_stripped_of_prefix(self): - if (sys.version_info.major, sys.version_info.minor) < (3, 13): - with pytest.warns(UserWarning): # noqa: PT030 - f = FilterObj("g:*.local") - else: - f = FilterObj("g:*.local") - assert f.target == "*.local" - # --------------------------------------------------------------------------- # FilterObj.from_file diff --git a/tests/test_version.py b/tests/test_version.py index da686fd..e816a84 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -2,5 +2,4 @@ def test_version_dev(): - """Smoke test to verify that the package version contains 'dev'.""" assert "dev" in dementor.__version__