From 830e160ee13fcf0b9398ec7932f4c683d25e5535 Mon Sep 17 00:00:00 2001 From: priv <140729444+scriptprivate@users.noreply.github.com> Date: Tue, 28 Jan 2025 09:06:57 -0300 Subject: [PATCH 01/12] fix: resolve PBP violations (#51) * fix: resolve PBP violations for boolean precedence and grep * fix: resolve PBP violations in subroutines and file handling * fix: add explicit returns to subroutines per PBP --- lib/Engine/Fuzzer.pm | 28 ++++++++++++++------------ lib/Engine/FuzzerThread.pm | 40 +++++++++++++++++++------------------- lib/Engine/Orchestrator.pm | 40 ++++++++++++++++++++++++-------------- 3 files changed, 61 insertions(+), 47 deletions(-) diff --git a/lib/Engine/Fuzzer.pm b/lib/Engine/Fuzzer.pm index f0e2eb2..d49993e 100755 --- a/lib/Engine/Fuzzer.pm +++ b/lib/Engine/Fuzzer.pm @@ -7,39 +7,41 @@ package Engine::Fuzzer { sub new { my ($self, $timeout, $headers, $skipssl, $proxy) = @_; - my $userAgent = Mojo::UserAgent -> new() -> request_timeout($timeout) -> insecure($skipssl); + my $userAgent = Mojo::UserAgent->new()->request_timeout($timeout)->insecure($skipssl); if ($proxy) { - $userAgent -> proxy -> http($proxy); - $userAgent -> proxy -> https($proxy); + $userAgent->proxy->http($proxy); + $userAgent->proxy->https($proxy); } - bless { + my $instance = bless { useragent => $userAgent, headers => $headers }, $self; + + return $instance; } sub request { my ($self, $method, $agent, $endpoint, $payload, $accept) = @_; - my $request = $self -> {useragent} -> build_tx ( + my $request = $self->{useragent}->build_tx( $method => $endpoint => { "User-Agent" => $agent, - %{$self -> {headers}} + %{$self->{headers}} } => $payload || "" ); try { - my $response = $self -> {useragent} -> start($request) -> result(); + my $response = $self->{useragent}->start($request)->result(); my $result = { "Method" => $method, "URL" => $endpoint, - "Code" => $response -> code(), - "Response" => $response -> message(), - "Content" => $response -> body(), - "Length" => $response -> headers() -> content_length() || "0" + "Code" => $response->code(), + "Response" => $response->message(), + "Content" => $response->body(), + "Length" => $response->headers()->content_length() || "0" }; return $result; @@ -47,7 +49,9 @@ package Engine::Fuzzer { catch { return 0; - } + }; + + return; } } diff --git a/lib/Engine/FuzzerThread.pm b/lib/Engine/FuzzerThread.pm index 8f10d7c..10525de 100644 --- a/lib/Engine/FuzzerThread.pm +++ b/lib/Engine/FuzzerThread.pm @@ -4,72 +4,72 @@ package Engine::FuzzerThread { use threads; use warnings; use Engine::Fuzzer; - + sub new { my ( $self, $queue, $target, $methods, $agent, $headers, $accept, $timeout, $return, $payload, $json, $delay, $exclude, $skipssl, $length, $content, $proxy ) = @_; - + my @verbs = split (/,/, $methods); my @valid_codes = split /,/, $return || ""; my @invalid_codes = split /,/, $exclude || ""; - + my $fuzzer = Engine::Fuzzer -> new($timeout, $headers, $skipssl, $proxy); my $format = JSON -> new() -> allow_nonref() -> pretty(); - + my $cmp; - + if ($length) { ($cmp, $length) = $length =~ /([>=<]{0,2})(\d+)/; - $cmp = sub { $_[0] >= $length } if ($cmp eq ">="); $cmp = sub { $_[0] <= $length } if ($cmp eq "<="); $cmp = sub { $_[0] != $length } if ($cmp eq "<>"); $cmp = sub { $_[0] > $length } if ($cmp eq ">"); $cmp = sub { $_[0] < $length } if ($cmp eq "<"); - $cmp = sub { $_[0] == $length } if (!$cmp or $cmp eq "="); + $cmp = sub { $_[0] == $length } if (defined($cmp) || $cmp eq "="); } - + async { while (defined(my $resource = $queue -> dequeue())) { - my $endpoint = $target . $resource; + my $endpoint = $target . $resource; my $found = 0; - + for my $verb (@verbs) { my $result = $fuzzer -> request($verb, $agent, $endpoint, $payload, $accept); - + unless ($result) { next; } - + my $status = $result -> {Code}; - - if (grep(/^$status$/, @invalid_codes) || ($return && !grep(/^$status$/, @valid_codes))) { + + if (scalar(grep { $_ eq $status } @invalid_codes) || ($return && !scalar(grep { $_ eq $status } @valid_codes))) { next; } - + if ($length && !($cmp -> ($result -> {Length}))) { next; } - + my $message = $json ? $format -> encode($result) : sprintf( "Code: %d | URL: %s | Method: %s | Response: %s | Length: %s", $status, $result -> {URL}, $result -> {Method}, $result -> {Response} || "?", $result -> {Length} ); - + print $message, "\n" if !$content || $result -> {Content} =~ m/$content/; - + sleep($delay); + $found = 1; } } }; - + return 1; } } -1; \ No newline at end of file +1; diff --git a/lib/Engine/Orchestrator.pm b/lib/Engine/Orchestrator.pm index 22a0ded..7ed1759 100644 --- a/lib/Engine/Orchestrator.pm +++ b/lib/Engine/Orchestrator.pm @@ -1,7 +1,8 @@ -package Engine::Orchestrator { +package Engine::Orchestrator { use strict; use threads; use warnings; + use Fcntl qw(O_RDONLY); use Engine::FuzzerThread; my $wordlist_queue; @@ -13,20 +14,22 @@ package Engine::Orchestrator { for (1 .. $number) { return unless (@{$list} > 0); - if (eof($list -> [0])) { + if (eof($list->[0])) { close shift @{$list}; - (@{$list} > 0) || $wordlist_queue -> end(); + (@{$list} > 0) || $wordlist_queue->end(); - next + next; } - my $filehandle = $list -> [0]; + my $filehandle = $list->[0]; chomp(my $line = <$filehandle>); - $wordlist_queue -> enqueue($line); + $wordlist_queue->enqueue($line); } + + return; } sub add_target { @@ -39,6 +42,8 @@ package Engine::Orchestrator { push @targets_queue, $target; } + + return; } sub run_fuzzer { @@ -51,27 +56,30 @@ package Engine::Orchestrator { $target = shift @targets_queue; } - $self -> threaded_fuzz($target, %options); + $self->threaded_fuzz($target, %options); } + + return 0; } sub threaded_fuzz { my ($self, $target, %options) = @_; my @current = map { - open(my $filehandle, "<$_") || die "$0: Can't open $_: $!"; + my $filehandle; + sysopen($filehandle, $_, O_RDONLY) || die "$0: Can't open $_: $!"; - $filehandle + $filehandle; } glob($options{wordlist}); - $wordlist_queue = Thread::Queue -> new(); + $wordlist_queue = Thread::Queue->new(); - use constant CONCURRENT_TASKS => 10; + my $CONCURRENT_TASKS = 10; - fill_queue(\@current, CONCURRENT_TASKS * $options{tasks}); + fill_queue(\@current, $CONCURRENT_TASKS * $options{tasks}); for (1 .. $options{tasks}) { - Engine::FuzzerThread -> new ( + Engine::FuzzerThread->new( $wordlist_queue, $target, $options{method}, @@ -92,11 +100,13 @@ package Engine::Orchestrator { ); } - while (threads -> list(threads::running) > 0) { + while (threads->list(threads::running) > 0) { fill_queue(\@current, $options{tasks}); } - map { $_ -> join() } threads -> list(threads::all); + map { $_->join() } threads->list(threads::all); + + close $_ for @current; return 0; } From 3789c7ab37f9bde29a1e3f511c6dfdd2949a6ff3 Mon Sep 17 00:00:00 2001 From: priv <140729444+scriptprivate@users.noreply.github.com> Date: Thu, 30 Jan 2025 19:56:39 -0300 Subject: [PATCH 02/12] fix: remove undef return --- lib/Engine/Fuzzer.pm | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/Engine/Fuzzer.pm b/lib/Engine/Fuzzer.pm index d49993e..3529f5b 100755 --- a/lib/Engine/Fuzzer.pm +++ b/lib/Engine/Fuzzer.pm @@ -32,10 +32,12 @@ package Engine::Fuzzer { } => $payload || "" ); + my $result; + try { my $response = $self->{useragent}->start($request)->result(); - - my $result = { + + $result = { "Method" => $method, "URL" => $endpoint, "Code" => $response->code(), @@ -43,16 +45,14 @@ package Engine::Fuzzer { "Content" => $response->body(), "Length" => $response->headers()->content_length() || "0" }; - - return $result; } - + catch { - return 0; + $result = 0; }; - - return; + + return $result; } } -1; \ No newline at end of file +1; From f9d91ecc1b6417a80ad9d08a959543f41dcac94a Mon Sep 17 00:00:00 2001 From: priv <140729444+scriptprivate@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:32:52 -0300 Subject: [PATCH 03/12] fix: JSON output --- lib/Engine/FuzzerThread.pm | 82 ++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/lib/Engine/FuzzerThread.pm b/lib/Engine/FuzzerThread.pm index 10525de..3b4740d 100644 --- a/lib/Engine/FuzzerThread.pm +++ b/lib/Engine/FuzzerThread.pm @@ -4,23 +4,26 @@ package Engine::FuzzerThread { use threads; use warnings; use Engine::Fuzzer; - + use threads::shared; + + my $is_first_json_element :shared = 1; + sub new { my ( $self, $queue, $target, $methods, $agent, $headers, $accept, $timeout, $return, $payload, $json, $delay, $exclude, $skipssl, $length, $content, $proxy ) = @_; - + my @verbs = split (/,/, $methods); my @valid_codes = split /,/, $return || ""; my @invalid_codes = split /,/, $exclude || ""; - + my $fuzzer = Engine::Fuzzer -> new($timeout, $headers, $skipssl, $proxy); my $format = JSON -> new() -> allow_nonref() -> pretty(); - + my $cmp; - + if ($length) { ($cmp, $length) = $length =~ /([>=<]{0,2})(\d+)/; $cmp = sub { $_[0] >= $length } if ($cmp eq ">="); @@ -28,46 +31,57 @@ package Engine::FuzzerThread { $cmp = sub { $_[0] != $length } if ($cmp eq "<>"); $cmp = sub { $_[0] > $length } if ($cmp eq ">"); $cmp = sub { $_[0] < $length } if ($cmp eq "<"); - $cmp = sub { $_[0] == $length } if (defined($cmp) || $cmp eq "="); + $cmp = sub { $_[0] == $length } if (defined($cmp) || $cmp eq "="); } - + async { while (defined(my $resource = $queue -> dequeue())) { - my $endpoint = $target . $resource; + my $endpoint = $target . $resource; my $found = 0; - + for my $verb (@verbs) { my $result = $fuzzer -> request($verb, $agent, $endpoint, $payload, $accept); - - unless ($result) { - next; - } - + + next unless $result; + my $status = $result -> {Code}; - - if (scalar(grep { $_ eq $status } @invalid_codes) || ($return && !scalar(grep { $_ eq $status } @valid_codes))) { - next; - } - - if ($length && !($cmp -> ($result -> {Length}))) { - next; - } - - my $message = $json ? $format -> encode($result) : sprintf( - "Code: %d | URL: %s | Method: %s | Response: %s | Length: %s", - $status, $result -> {URL}, $result -> {Method}, - $result -> {Response} || "?", $result -> {Length} - ); - - print $message, "\n" if !$content || $result -> {Content} =~ m/$content/; - - sleep($delay); - + + next if scalar(grep { $_ eq $status } @invalid_codes) || + ($return && !scalar(grep { $_ eq $status } @valid_codes)); + + next if $length && !($cmp -> ($result -> {Length})); + $found = 1; + sleep($delay); + + my $output_handler = sub { + my ($result) = @_; + lock($is_first_json_element); + + my $json_str = $format -> encode($result); + my $output = $is_first_json_element ? + $json_str : + ",\n$json_str"; + + $is_first_json_element = 0; + return $output; + }; + + my $plain_handler = sub { + my ($result) = @_; + return sprintf( + "Code: %d | URL: %s | Method: %s | Response: %s | Length: %s\n", + $status, $result -> {URL}, $result -> {Method}, + $result -> {Response} || "?", $result -> {Length} + ); + }; + + my $handler = $json ? $output_handler : $plain_handler; + print $handler->($result); } } }; - + return 1; } } From dc37b735fe0eb3e8278224f73157bacb722be472 Mon Sep 17 00:00:00 2001 From: priv <140729444+scriptprivate@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:33:53 -0300 Subject: [PATCH 04/12] fix: JSON output --- nozaki.pl | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/nozaki.pl b/nozaki.pl index de92591..3388674 100755 --- a/nozaki.pl +++ b/nozaki.pl @@ -5,6 +5,7 @@ use threads; use warnings; use Thread::Queue; +use IO::Interactive; use Find::Lib "./lib"; use Functions::Helper; use Functions::Parser; @@ -48,6 +49,8 @@ sub main { return Functions::Helper -> new() unless @targets; + print "[\n" if $options{json}; + if ($workflow) { my $rules = Functions::Parser -> new($workflow); @@ -66,7 +69,14 @@ sub main { Engine::Orchestrator::add_target(@targets); - return Engine::Orchestrator -> run_fuzzer(%options); + Engine::Orchestrator -> run_fuzzer(%options); + + if ($options{json}) { + print "]\n"; + truncate STDOUT, tell(STDOUT) - 2 if IO::Interactive::is_interactive(); + } + + return 0; } -exit main() unless caller; \ No newline at end of file +exit main() unless caller; From 40705dd17c2759d90d3c840f19ac0d8e0e6d8e88 Mon Sep 17 00:00:00 2001 From: priv <140729444+scriptprivate@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:34:28 -0300 Subject: [PATCH 05/12] update cpanfile --- cpanfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpanfile b/cpanfile index a3656ca..06b51bd 100644 --- a/cpanfile +++ b/cpanfile @@ -1,4 +1,5 @@ requires "Mojo::UserAgent"; requires "YAML::Tiny", "1.73"; requires "Find::Lib", "1.04"; -requires "JSON", "4.07"; \ No newline at end of file +requires "JSON", "4.07"; +requires "IO::Interactive"; From e4653963f4fc48a59133beafbe61cbce7c8cee30 Mon Sep 17 00:00:00 2001 From: priv <140729444+scriptprivate@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:05:42 -0300 Subject: [PATCH 06/12] feat: add unit test for JSON validation --- tests/json_output.t | 73 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/json_output.t diff --git a/tests/json_output.t b/tests/json_output.t new file mode 100644 index 0000000..8fc6fdf --- /dev/null +++ b/tests/json_output.t @@ -0,0 +1,73 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use JSON; +use Test::More; +use Test::MockModule; +use File::Temp qw/ tempfile /; + +use lib '../lib/'; + +my ($fh, $wordlist_filename) = tempfile(); +print $fh "/test1\n/test2\n/test3\n"; +close $fh; + +my $mock_multi_result = [ + { + Method => "GET", + URL => "http://example.com/test1", + Length => "1024", + Code => "200", + Content => "Sample content 1", + Response => "OK" + }, + { + Method => "POST", + URL => "http://example.com/test2", + Length => "512", + Code => "404", + Content => "Not found", + Response => "Not Found" + }, + { + Method => "PUT", + URL => "http://example.com/test3", + Length => "256", + Code => "403", + Content => "Forbidden", + Response => "Forbidden" + } +]; + +my $mock_helper = Test::MockModule->new('Functions::Helper'); +$mock_helper->mock('new', sub { return $wordlist_filename }); + +sub test_multiple_items_json { + my $mock_fuzzer = Test::MockModule->new('Engine::FuzzerThread'); + $mock_fuzzer->mock('new', sub { + return sub { return $mock_multi_result } + }); + + my $json_output = `$^X ../nozaki.pl -j -u http://example.com/ -w $wordlist_filename 2>/dev/null`; + + my $results = eval { decode_json($json_output) }; + ok(!$@, "Multiple items JSON is valid") or diag "Decoding error: $@"; + cmp_ok(scalar @$results, '>', 1, "Multiple items array contains more than one element"); + + foreach my $result (@$results) { + ok(exists $result->{Method}, "Result has Method"); + ok(exists $result->{URL}, "Result has URL"); + ok(exists $result->{Length}, "Result has Length"); + ok(exists $result->{Code}, "Result has Code"); + ok(exists $result->{Content}, "Result has Content"); + ok(exists $result->{Response}, "Result has Response"); + } + + return; +} + +test_multiple_items_json(); + +END { unlink $wordlist_filename if $wordlist_filename; } + +done_testing(); From 382151884ea461f8b95275ccb476dd0177e0a201 Mon Sep 17 00:00:00 2001 From: priv <140729444+scriptprivate@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:09:53 -0300 Subject: [PATCH 07/12] chore: add test dependencies --- cpanfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cpanfile b/cpanfile index 06b51bd..6341fbb 100644 --- a/cpanfile +++ b/cpanfile @@ -3,3 +3,8 @@ requires "YAML::Tiny", "1.73"; requires "Find::Lib", "1.04"; requires "JSON", "4.07"; requires "IO::Interactive"; + +on 'test' => sub { + requires "Test::More"; + requires "Test::MockModule"; +}; From 82a945ae0b7a5db3154066af7759f9caaa38c12f Mon Sep 17 00:00:00 2001 From: priv <140729444+scriptprivate@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:12:15 -0300 Subject: [PATCH 08/12] ci: add workflow for running test suite --- .github/workflows/test-suite.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/test-suite.yml diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml new file mode 100644 index 0000000..ebb6352 --- /dev/null +++ b/.github/workflows/test-suite.yml @@ -0,0 +1,27 @@ +name: Test Suite + +on: + pull_request: + branches: + - main + - develop + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Perl + run: | + sudo apt-get update + sudo apt-get install -y perl + + - name: Install dependencies + run: cpanm --installdeps --with-test . + + - name: Run tests + working-directory: ./tests + run: prove -r From 0a462405e2944be93f858b5acb23dbc6848869af Mon Sep 17 00:00:00 2001 From: priv <140729444+scriptprivate@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:18:28 -0300 Subject: [PATCH 09/12] fix: add cpanm install --- .github/workflows/test-suite.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index ebb6352..ee3d5fa 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -18,6 +18,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y perl + sudo apt install -y perl cpanminus - name: Install dependencies run: cpanm --installdeps --with-test . From 2e7936a06b1f3b7af09d4cb0856f705ff7638057 Mon Sep 17 00:00:00 2001 From: priv <140729444+scriptprivate@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:22:29 -0300 Subject: [PATCH 10/12] fix: install dependencies with sudo --- .github/workflows/test-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index ee3d5fa..d9a83f1 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -21,7 +21,7 @@ jobs: sudo apt install -y perl cpanminus - name: Install dependencies - run: cpanm --installdeps --with-test . + run: sudo cpanm --installdeps --with-test . - name: Run tests working-directory: ./tests From 08ba7587872148f49b2c15a1a8546e97e046e691 Mon Sep 17 00:00:00 2001 From: priv <140729444+scriptprivate@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:44:08 -0300 Subject: [PATCH 11/12] resolve suggestions --- lib/Engine/FuzzerThread.pm | 66 +++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/lib/Engine/FuzzerThread.pm b/lib/Engine/FuzzerThread.pm index 3b4740d..917d60d 100644 --- a/lib/Engine/FuzzerThread.pm +++ b/lib/Engine/FuzzerThread.pm @@ -7,58 +7,66 @@ package Engine::FuzzerThread { use threads::shared; my $is_first_json_element :shared = 1; - + sub new { my ( $self, $queue, $target, $methods, $agent, $headers, $accept, $timeout, $return, $payload, $json, $delay, $exclude, $skipssl, $length, $content, $proxy ) = @_; - + my @verbs = split (/,/, $methods); my @valid_codes = split /,/, $return || ""; my @invalid_codes = split /,/, $exclude || ""; - - my $fuzzer = Engine::Fuzzer -> new($timeout, $headers, $skipssl, $proxy); - my $format = JSON -> new() -> allow_nonref() -> pretty(); + + my $fuzzer = Engine::Fuzzer->new($timeout, $headers, $skipssl, $proxy); + my $format = JSON->new()->allow_nonref()->pretty(); my $cmp; - + if ($length) { ($cmp, $length) = $length =~ /([>=<]{0,2})(\d+)/; $cmp = sub { $_[0] >= $length } if ($cmp eq ">="); $cmp = sub { $_[0] <= $length } if ($cmp eq "<="); $cmp = sub { $_[0] != $length } if ($cmp eq "<>"); - $cmp = sub { $_[0] > $length } if ($cmp eq ">"); - $cmp = sub { $_[0] < $length } if ($cmp eq "<"); + $cmp = sub { $_[0] > $length } if ($cmp eq ">"); + $cmp = sub { $_[0] < $length } if ($cmp eq "<"); $cmp = sub { $_[0] == $length } if (defined($cmp) || $cmp eq "="); } - + async { - while (defined(my $resource = $queue -> dequeue())) { + while (defined(my $resource = $queue->dequeue())) { my $endpoint = $target . $resource; my $found = 0; - + for my $verb (@verbs) { - my $result = $fuzzer -> request($verb, $agent, $endpoint, $payload, $accept); - - next unless $result; - - my $status = $result -> {Code}; - - next if scalar(grep { $_ eq $status } @invalid_codes) || - ($return && !scalar(grep { $_ eq $status } @valid_codes)); - - next if $length && !($cmp -> ($result -> {Length})); - + my $result = $fuzzer->request($verb, $agent, $endpoint, $payload, $accept); + + unless ($result) { + next; + } + + my $status = $result->{Code}; + + if (scalar(grep { $_ eq $status } @invalid_codes)) { + next; + } + if ($return && !scalar(grep { $_ eq $status } @valid_codes)) { + next; + } + + if ($length && !($cmp->($result->{Length}))) { + next; + } + $found = 1; sleep($delay); - + my $output_handler = sub { my ($result) = @_; lock($is_first_json_element); - my $json_str = $format -> encode($result); + my $json_str = $format->encode($result); my $output = $is_first_json_element ? $json_str : ",\n$json_str"; @@ -66,22 +74,22 @@ package Engine::FuzzerThread { $is_first_json_element = 0; return $output; }; - + my $plain_handler = sub { my ($result) = @_; return sprintf( "Code: %d | URL: %s | Method: %s | Response: %s | Length: %s\n", - $status, $result -> {URL}, $result -> {Method}, - $result -> {Response} || "?", $result -> {Length} + $status, $result->{URL}, $result->{Method}, + $result->{Response} || "?", $result->{Length} ); }; - + my $handler = $json ? $output_handler : $plain_handler; print $handler->($result); } } }; - + return 1; } } From 9148a5f49b7bfd69422f9546f6ccac348ad36bbb Mon Sep 17 00:00:00 2001 From: priv <140729444+scriptprivate@users.noreply.github.com> Date: Tue, 4 Feb 2025 08:48:35 -0300 Subject: [PATCH 12/12] fix: improve JSON validation - add retry mechanism to handle race conditions in JSON parsing - log nozaki.pl output to debug.log for debugging - ensure debug.log is deleted after test completion --- tests/json_output.t | 66 ++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/tests/json_output.t b/tests/json_output.t index 8fc6fdf..6055fc5 100644 --- a/tests/json_output.t +++ b/tests/json_output.t @@ -12,29 +12,31 @@ my ($fh, $wordlist_filename) = tempfile(); print $fh "/test1\n/test2\n/test3\n"; close $fh; +ok(-e $wordlist_filename, "Wordlist file exists before execution"); + my $mock_multi_result = [ { - Method => "GET", - URL => "http://example.com/test1", - Length => "1024", - Code => "200", - Content => "Sample content 1", + Method => "GET", + URL => "http://example.com/test1", + Length => "1024", + Code => "200", + Content => "Sample content 1", Response => "OK" }, { - Method => "POST", - URL => "http://example.com/test2", - Length => "512", - Code => "404", - Content => "Not found", + Method => "POST", + URL => "http://example.com/test2", + Length => "512", + Code => "404", + Content => "Not found", Response => "Not Found" }, { - Method => "PUT", - URL => "http://example.com/test3", - Length => "256", - Code => "403", - Content => "Forbidden", + Method => "PUT", + URL => "http://example.com/test3", + Length => "256", + Code => "403", + Content => "Forbidden", Response => "Forbidden" } ]; @@ -44,17 +46,30 @@ $mock_helper->mock('new', sub { return $wordlist_filename }); sub test_multiple_items_json { my $mock_fuzzer = Test::MockModule->new('Engine::FuzzerThread'); - $mock_fuzzer->mock('new', sub { - return sub { return $mock_multi_result } - }); + $mock_fuzzer->mock('new', sub { return bless {}, 'Engine::FuzzerThread' }); + $mock_fuzzer->mock('run', sub { return $mock_multi_result }); + + my $json_output; + my $decoded_json; + my $debug_log = "debug.log"; + + for (1..5) { + $json_output = `$^X ../nozaki.pl -j -u http://example.com/ -w $wordlist_filename 2>/dev/null`; + + open my $log, '>', $debug_log; + print $log $json_output; + close $log; + + eval { $decoded_json = decode_json($json_output) }; + last unless $@; + + sleep 1; + } - my $json_output = `$^X ../nozaki.pl -j -u http://example.com/ -w $wordlist_filename 2>/dev/null`; - - my $results = eval { decode_json($json_output) }; ok(!$@, "Multiple items JSON is valid") or diag "Decoding error: $@"; - cmp_ok(scalar @$results, '>', 1, "Multiple items array contains more than one element"); + cmp_ok(scalar @$decoded_json, '>', 1, "Multiple items array contains more than one element"); - foreach my $result (@$results) { + foreach my $result (@$decoded_json) { ok(exists $result->{Method}, "Result has Method"); ok(exists $result->{URL}, "Result has URL"); ok(exists $result->{Length}, "Result has Length"); @@ -68,6 +83,9 @@ sub test_multiple_items_json { test_multiple_items_json(); -END { unlink $wordlist_filename if $wordlist_filename; } +END { + unlink $wordlist_filename if $wordlist_filename; + unlink "debug.log" if -e "debug.log"; +} done_testing();