diff --git a/security-reports/zap-baseline.html b/security-reports/zap-baseline.html new file mode 100644 index 00000000..1123d0ca --- /dev/null +++ b/security-reports/zap-baseline.html @@ -0,0 +1,621 @@ + + + + +ZAP Scanning Report + + + +

+ + + ZAP Scanning Report +

+

+ + +

+ + Site: http://host.docker.internal:5173 + +

+ +

+ Generated on Mon, 8 Jun 2026 05:33:02 +

+ +

+ ZAP Version: 2.17.0 +

+ +

+ ZAP by Checkmarx +

+ + +

Summary of Alerts

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Risk LevelNumber of Alerts
+
High
+
+
0
+
+
Medium
+
+
0
+
+
Low
+
+
0
+
+
Informational
+
+
1
+
+
False Positives:
+
+
0
+
+
+ + + +

Insights

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LevelReasonSiteDescriptionStatistic
+
Info
+
+
Informational
+
+
http://host.docker.internal:5173
+
+
Percentage of responses with status code 4xx
+
+
100 %
+
+
Info
+
+
Informational
+
+
http://host.docker.internal:5173
+
+
Percentage of endpoints with content type text/plain
+
+
100 %
+
+
Info
+
+
Informational
+
+
http://host.docker.internal:5173
+
+
Percentage of endpoints with method GET
+
+
100 %
+
+
Info
+
+
Informational
+
+
http://host.docker.internal:5173
+
+
Count of total endpoints
+
+
2
+
+
+ + + + +

Summary of Sequences

+

For each step: result (Pass/Fail) - risk (of highest alert(s) for the step, if any).

+ + + + + + + + +

Alerts

+ + + + + + + + + + + + + + +
NameRisk LevelNumber of Instances
Non-Storable ContentInformational3
+
+ + + +

Alert Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Informational
Non-Storable Content
Description +
The response contents are not storable by caching components such as proxy servers. If the response does not contain sensitive, personal or user-specific information, it may benefit from being stored and cached, to improve performance.
+ +
URLhttp://host.docker.internal:5173
Node Namehttp://host.docker.internal:5173
MethodGET
Parameter
Attack
Evidence403
Other Info
URLhttp://host.docker.internal:5173/robots.txt
Node Namehttp://host.docker.internal:5173/robots.txt
MethodGET
Parameter
Attack
Evidence403
Other Info
URLhttp://host.docker.internal:5173/sitemap.xml
Node Namehttp://host.docker.internal:5173/sitemap.xml
MethodGET
Parameter
Attack
Evidence403
Other Info
Instances3
Solution +
The content may be marked as storable by ensuring that the following conditions are satisfied:
+
+ +
The request method must be understood by the cache and defined as being cacheable ("GET", "HEAD", and "POST" are currently defined as cacheable)
+
+ +
The response status code must be understood by the cache (one of the 1XX, 2XX, 3XX, 4XX, or 5XX response classes are generally understood)
+
+ +
The "no-store" cache directive must not appear in the request or response header fields
+
+ +
For caching by "shared" caches such as "proxy" caches, the "private" response directive must not appear in the response
+
+ +
For caching by "shared" caches such as "proxy" caches, the "Authorization" header field must not appear in the request, unless the response explicitly allows it (using one of the "must-revalidate", "public", or "s-maxage" Cache-Control response directives)
+
+ +
In addition to the conditions above, at least one of the following conditions must also be satisfied by the response:
+
+ +
It must contain an "Expires" header field
+
+ +
It must contain a "max-age" response directive
+
+ +
For "shared" caches such as "proxy" caches, it must contain a "s-maxage" response directive
+
+ +
It must contain a "Cache Control Extension" that allows it to be cached
+
+ +
It must have a status code that is defined as cacheable by default (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501).
+ +
Reference + https://datatracker.ietf.org/doc/html/rfc7234 +
+ + https://datatracker.ietf.org/doc/html/rfc7231 +
+ + https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html + +
CWE Id524
WASC Id13
Plugin Id10049
+
+ + + + + +

Sequence Details

+ With the associated active scan results. + + + +
+ + + + + + + diff --git a/security-reports/zap-baseline.json b/security-reports/zap-baseline.json new file mode 100644 index 00000000..2544e164 --- /dev/null +++ b/security-reports/zap-baseline.json @@ -0,0 +1,103 @@ +{ + "@programName": "ZAP", + "@version": "2.17.0", + "@generated": "Mon, 8 Jun 2026 05:33:02", + "created": "2026-06-08T05:33:02.123437092Z", + "insights":[ + { + "level": "Info", + "reason": "Informational", + "site": "http://host.docker.internal:5173", + "key": "insight.code.4xx", + "description": "Percentage of responses with status code 4xx", + "statistic": "100" + }, + { + "level": "Info", + "reason": "Informational", + "site": "http://host.docker.internal:5173", + "key": "insight.endpoint.ctype.text/plain", + "description": "Percentage of endpoints with content type text/plain", + "statistic": "100" + }, + { + "level": "Info", + "reason": "Informational", + "site": "http://host.docker.internal:5173", + "key": "insight.endpoint.method.GET", + "description": "Percentage of endpoints with method GET", + "statistic": "100" + }, + { + "level": "Info", + "reason": "Informational", + "site": "http://host.docker.internal:5173", + "key": "insight.endpoint.total", + "description": "Count of total endpoints", + "statistic": "2" + } + ], + "site":[ + { + "@name": "http://host.docker.internal:5173", + "@host": "host.docker.internal", + "@port": "5173", + "@ssl": "false", + "alerts": [ + { + "pluginid": "10049", + "alertRef": "10049-1", + "alert": "Non-Storable Content", + "name": "Non-Storable Content", + "riskcode": "0", + "confidence": "2", + "riskdesc": "Informational (Medium)", + "desc": "

The response contents are not storable by caching components such as proxy servers. If the response does not contain sensitive, personal or user-specific information, it may benefit from being stored and cached, to improve performance.

", + "instances":[ + { + "id": "3", + "uri": "http://host.docker.internal:5173", + "nodeName": "http:\/\/host.docker.internal:5173", + "method": "GET", + "param": "", + "attack": "", + "evidence": "403", + "otherinfo": "" + }, + { + "id": "2", + "uri": "http://host.docker.internal:5173/robots.txt", + "nodeName": "http:\/\/host.docker.internal:5173\/robots.txt", + "method": "GET", + "param": "", + "attack": "", + "evidence": "403", + "otherinfo": "" + }, + { + "id": "1", + "uri": "http://host.docker.internal:5173/sitemap.xml", + "nodeName": "http:\/\/host.docker.internal:5173\/sitemap.xml", + "method": "GET", + "param": "", + "attack": "", + "evidence": "403", + "otherinfo": "" + } + ], + "count": "3", + "systemic": false, + "solution": "

The content may be marked as storable by ensuring that the following conditions are satisfied:

The request method must be understood by the cache and defined as being cacheable (\"GET\", \"HEAD\", and \"POST\" are currently defined as cacheable)

The response status code must be understood by the cache (one of the 1XX, 2XX, 3XX, 4XX, or 5XX response classes are generally understood)

The \"no-store\" cache directive must not appear in the request or response header fields

For caching by \"shared\" caches such as \"proxy\" caches, the \"private\" response directive must not appear in the response

For caching by \"shared\" caches such as \"proxy\" caches, the \"Authorization\" header field must not appear in the request, unless the response explicitly allows it (using one of the \"must-revalidate\", \"public\", or \"s-maxage\" Cache-Control response directives)

In addition to the conditions above, at least one of the following conditions must also be satisfied by the response:

It must contain an \"Expires\" header field

It must contain a \"max-age\" response directive

For \"shared\" caches such as \"proxy\" caches, it must contain a \"s-maxage\" response directive

It must contain a \"Cache Control Extension\" that allows it to be cached

It must have a status code that is defined as cacheable by default (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501).

", + "otherinfo": "", + "reference": "

https://datatracker.ietf.org/doc/html/rfc7234

https://datatracker.ietf.org/doc/html/rfc7231

https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html

", + "cweid": "524", + "wascid": "13", + "sourceid": "1" + } + ] + } + ], + "sequences":[ + ] + +} diff --git a/security-reports/zap.yaml b/security-reports/zap.yaml new file mode 100644 index 00000000..1f041ef0 --- /dev/null +++ b/security-reports/zap.yaml @@ -0,0 +1,40 @@ +env: + contexts: + - excludePaths: [] + name: baseline + urls: + - http://host.docker.internal:4173 + parameters: + failOnError: true + progressToStdout: false +jobs: +- parameters: + enableTags: false + maxAlertsPerRule: 10 + type: passiveScan-config +- parameters: + maxDuration: 1 + url: http://host.docker.internal:4173 + type: spider +- parameters: + maxDuration: 0 + type: passiveScan-wait +- parameters: + format: Long + summaryFile: /home/zap/zap_out.json + rules: [] + type: outputSummary +- parameters: + reportDescription: '' + reportDir: /zap/wrk/ + reportFile: zap-preview.html + reportTitle: ZAP Scanning Report + template: traditional-html + type: report +- parameters: + reportDescription: '' + reportDir: /zap/wrk/ + reportFile: zap-preview.json + reportTitle: ZAP Scanning Report + template: traditional-json + type: report diff --git a/tests/Feature/BulkUserRoleCreationTest_api.php b/tests/Feature/BulkUserRoleCreationTest_api.php new file mode 100644 index 00000000..2295f6af --- /dev/null +++ b/tests/Feature/BulkUserRoleCreationTest_api.php @@ -0,0 +1,125 @@ +seedRolesForUserCreation(); + $adminActor = $this->createAuthenticatedAdministratorActor($roles['administrator']); + + $this->actingAs($adminActor, 'api'); + + $this->createUsersThroughApi('parent', (int) $roles['parent']->id, 1000); + $this->createUsersThroughApi('teacher', (int) $roles['teacher']->id, 1000); + $this->createUsersThroughApi('admin', (int) $roles['admin']->id, 1000); + + $this->assertSame(3000, User::query()->where('email', 'like', 'bulk_api_%')->count()); + $this->assertSame(1000, User::query()->hasRoleName('parent')->where('email', 'like', 'bulk_api_parent_%')->count()); + $this->assertSame(1000, User::query()->hasRoleName('teacher')->where('email', 'like', 'bulk_api_teacher_%')->count()); + $this->assertSame(1000, User::query()->hasRoleName('admin')->where('email', 'like', 'bulk_api_admin_%')->count()); + + $this->assertDatabaseHas('users', [ + 'email' => 'bulk_api_parent_0001@example.test', + 'status' => 'Active', + 'is_verified' => 1, + ]); + + $this->assertSame( + ['parent'], + User::query()->where('email', 'bulk_api_parent_0001@example.test')->firstOrFail()->roleNames() + ); + $this->assertSame( + ['teacher'], + User::query()->where('email', 'bulk_api_teacher_0001@example.test')->firstOrFail()->roleNames() + ); + $this->assertSame( + ['admin'], + User::query()->where('email', 'bulk_api_admin_0001@example.test')->firstOrFail()->roleNames() + ); + } + + /** + * @return array + */ + private function seedRolesForUserCreation(): array + { + return collect([ + 'administrator' => ['name' => 'administrator', 'slug' => 'administrator', 'priority' => 5], + 'admin' => ['name' => 'admin', 'slug' => 'admin', 'priority' => 10], + 'teacher' => ['name' => 'teacher', 'slug' => 'teacher', 'priority' => 20], + 'parent' => ['name' => 'parent', 'slug' => 'parent', 'priority' => 30], + ])->mapWithKeys(function (array $attributes, string $key): array { + return [ + $key => Role::query()->create($attributes + [ + 'description' => ucfirst(str_replace('_', ' ', $key)), + 'dashboard_route' => 'administrator/administratordashboard', + 'is_active' => 1, + ]), + ]; + })->all(); + } + + private function createAuthenticatedAdministratorActor(Role $administratorRole): User + { + $adminActor = User::factory()->create([ + 'email' => 'bulk-api-test-actor@example.test', + 'status' => 'Active', + 'is_verified' => 1, + ]); + + $adminActor->roles()->attach($administratorRole->id); + + return $adminActor; + } + + private function createUsersThroughApi(string $roleName, int $roleId, int $count): void + { + for ($index = 1; $index <= $count; $index++) { + $response = $this->postJson('/api/v1/users', $this->payloadForUser($roleName, $roleId, $index)); + + $response + ->assertCreated() + ->assertJsonPath('ok', true); + } + } + + /** + * @return array + */ + private function payloadForUser(string $roleName, int $roleId, int $index): array + { + $number = str_pad((string) $index, 4, '0', STR_PAD_LEFT); + $name = ucfirst($roleName); + + return [ + 'firstname' => "Bulk{$name}", + 'lastname' => "User{$number}", + 'gender' => $index % 2 === 0 ? 'Female' : 'Male', + 'cellphone' => '555' . str_pad((string) $index, 7, '0', STR_PAD_LEFT), + 'email' => "bulk_api_{$roleName}_{$number}@example.test", + 'address_street' => "{$index} Test Street", + 'city' => 'Brooklyn', + 'state' => 'NY', + 'zip' => '11201', + 'accept_school_policy' => true, + 'role_id' => $roleId, + 'password' => 'password', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + 'school_id' => 1, + 'user_type' => 'primary', + 'status' => 'Active', + 'is_verified' => true, + 'is_suspended' => false, + ]; + } +}