Skip to content

Commit 66336a2

Browse files
CyberSecDefclaude
andcommitted
test: add Api/Stig controller tests; fix current()-on-object + null 404s
Add functional coverage for ApiController and StigController (happy paths for /api, CCI, RMF v4/v5, STIG list/summary/vuln, plus unknown-version 404s). Writing the tests surfaced two pre-existing issues, now fixed: - current() called on a SimpleXMLElement is deprecated as of PHP 8.4. Replace the 14 current($object) sites in ApiController with direct (string) casts / attribute access. The current($x->xpath(...)) calls take arrays and are left as-is. Output verified byte-identical across rmf/4, rmf/5, stig summary, stig vuln and scap vuln (only the intentionally-random /api/stig/tip differs). - A missing version/release left array_pop() returning null, so $stig->filename warned ("property on null") before the file guard. Add an explicit null check that 404s first, in ApiController, StigController and ScapController. Also ignore the subdirectory .phpunit.cache (the root-anchored rule didn't reach cyber.trackr.live/). Verified on PHP 8.4 (deploy target): OK (26 tests, 2065 assertions), no warnings or deprecations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8710c31 commit 66336a2

6 files changed

Lines changed: 196 additions & 14 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
/cyber.trackr.live/var/
4040
/cyber.trackr.live/vendor/
4141
/cyber.trackr.live/node_modules/
42+
/cyber.trackr.live/.phpunit.cache/
4243

4344
# Air-gapped image tarballs produced by cyber.trackr.live/bin/build-image.sh
4445
# (multi-GB `docker save` output) — generated artifacts, never committed.

cyber.trackr.live/src/Controller/ApiController.php

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ public function api_rmfv4_show($con): Response
253253

254254
$results['enhancements'] = [];
255255
foreach ($controls_xml->xpath("./aaa:control-enhancements/aaa:control-enhancement") as $enhancement) {
256-
$results['enhancements'][current($enhancement->number)] = current($enhancement->title);
256+
$results['enhancements'][(string)$enhancement->number] = (string)$enhancement->title;
257257
}
258258

259259
return new Response(json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), Response::HTTP_OK, ['content-type' => 'application/json'] );
@@ -385,15 +385,15 @@ public function api_rmf_show($con): Response
385385
$results['references'] = [];
386386
foreach ($controls_xml->xpath("./xmlns:references/xmlns:reference") as $ref) {
387387
$results['references'][] = [
388-
"href" => str_replace("\/", "/", current($ref->item)['href']),
389-
"text" => current($ref->item->text),
390-
"name" => current($ref->short_name)
388+
"href" => str_replace("\/", "/", (string)$ref->item['href']),
389+
"text" => (string)$ref->item->text,
390+
"name" => (string)$ref->short_name
391391
];
392392
}
393393

394394
$results['enhancements'] = [];
395395
foreach ($controls_xml->xpath("./xmlns:control-enhancements/xmlns:control-enhancement") as $enhancement) {
396-
$results['enhancements'][current($enhancement->number)] = current($enhancement->title);
396+
$results['enhancements'][(string)$enhancement->number] = (string)$enhancement->title;
397397
}
398398

399399
return new Response(json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), Response::HTTP_OK, ['content-type' => 'application/json'] );
@@ -541,9 +541,9 @@ public function api_stig_tip(): Response
541541
$vuln_id = (string)$group->attributes()['id'];
542542

543543
$collection[$key]->tip = [
544-
"title" => (string)current($group->Rule->title),
545-
"rule" => (string)current($group->Rule->attributes()['id']),
546-
"severity" => (string)current($group->Rule->attributes()['severity']),
544+
"title" => (string)$group->Rule->title,
545+
"rule" => (string)$group->Rule->attributes()['id'],
546+
"severity" => (string)$group->Rule->attributes()['severity'],
547547
"vuln_id" => $vuln_id,
548548
];
549549

@@ -573,6 +573,9 @@ public function api_stig_summary($title, $version, $release): Response
573573
return false;
574574
});
575575
$stig = array_pop($stig);
576+
if ($stig === null) {
577+
throw $this->createNotFoundException('The STIG does not exist');
578+
}
576579
$stig_filename = realpath(__DIR__ . "/../../resources/data/stig/" . $stig->filename);
577580
if ($stig_filename === false || !is_file($stig_filename)) {
578581
throw $this->createNotFoundException('The STIG does not exist');
@@ -598,17 +601,17 @@ public function api_stig_summary($title, $version, $release): Response
598601

599602
$results["profiles"] = [];
600603
foreach ($stig_xml->xpath('/xmlns:Benchmark/xmlns:Profile') as $profile) {
601-
$results["profiles"][(string)$profile->attributes()['id']] = (string)current($profile->title);
604+
$results["profiles"][(string)$profile->attributes()['id']] = (string)$profile->title;
602605
}
603606

604607
$results["requirements"] = [];
605608
foreach ($stig_xml->xpath('/xmlns:Benchmark/xmlns:Group') as $group) {
606609
$vuln_id = (string)$group->attributes()['id'];
607610

608611
$results["requirements"][$vuln_id] = [
609-
"title" => (string)current($group->Rule->title),
610-
"rule" => (string)current($group->Rule->attributes()['id']),
611-
"severity" => (string)current($group->Rule->attributes()['severity']),
612+
"title" => (string)$group->Rule->title,
613+
"rule" => (string)$group->Rule->attributes()['id'],
614+
"severity" => (string)$group->Rule->attributes()['severity'],
612615
"link" => "/stig/{$title}/{$version}/{$release}/{$vuln_id}"
613616
];
614617
}
@@ -634,6 +637,9 @@ public function api_stig_vuln(Request $request, $title, $version, $release, $vul
634637
return false;
635638
});
636639
$stig = array_pop($stig);
640+
if ($stig === null) {
641+
throw $this->createNotFoundException('The STIG does not exist');
642+
}
637643
$stig_filename = realpath(__DIR__ . "/../../resources/data/stig/" . $stig->filename);
638644
if ($stig_filename === false || !is_file($stig_filename)) {
639645
throw $this->createNotFoundException('The STIG does not exist');
@@ -659,7 +665,7 @@ public function api_stig_vuln(Request $request, $title, $version, $release, $vul
659665
$results["stig-published"] = (string)current($stig_xml->xpath('/xmlns:Benchmark/xmlns:status/@date'));
660666

661667
$vuln = current($stig_xml->xpath("/xmlns:Benchmark/xmlns:Group[@id='{$vuln}']"));
662-
$results["id"] = (string)current($vuln->attributes()['id']);
668+
$results["id"] = (string)$vuln->attributes()['id'];
663669
$results["group"] = (string)$vuln->title;
664670
$results["rule"] = (string)$vuln->Rule->attributes()['id'];
665671
$results["severity"] = (string)$vuln->Rule->attributes()['severity'];
@@ -725,6 +731,9 @@ public function api_scap_summary($title, $version, $release): Response
725731
return false;
726732
});
727733
$scap = array_pop($scap);
734+
if ($scap === null) {
735+
throw $this->createNotFoundException('The SCAP does not exist');
736+
}
728737
$scap_filename = realpath(__DIR__ . "/../../resources/data/scap/" . $scap->filename);
729738
if ($scap_filename === false || !is_file($scap_filename)) {
730739
throw $this->createNotFoundException('The SCAP does not exist');
@@ -786,6 +795,9 @@ public function api_scap_vuln($title, $version, $release, $vuln): Response
786795
return false;
787796
});
788797
$scap = array_pop($scap);
798+
if ($scap === null) {
799+
throw $this->createNotFoundException('The SCAP does not exist');
800+
}
789801
$scap_filename = realpath(__DIR__ . "/../../resources/data/scap/" . $scap->filename);
790802
if ($scap_filename === false || !is_file($scap_filename)) {
791803
throw $this->createNotFoundException('The SCAP does not exist');
@@ -816,7 +828,7 @@ public function api_scap_vuln($title, $version, $release, $vuln): Response
816828

817829
$vuln = current($scap_xml->xpath("//xccdf:Benchmark/xccdf:Group[@id='xccdf_mil.disa.stig_group_{$vuln}']"));
818830

819-
$results["id"] = (string)current($vuln->attributes()['id']);
831+
$results["id"] = (string)$vuln->attributes()['id'];
820832
$results["group"] = (string)current($vuln->xpath("./xccdf:title"));
821833
$results["rule"] = (string)current($vuln->xpath("./xccdf:Rule/@id"));
822834
$results["severity"] = (string)current($vuln->xpath("./xccdf:Rule/@severity"));

cyber.trackr.live/src/Controller/ScapController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ public function scap_download($title, $version, $release): Response
7373
});
7474

7575
$scap = array_pop($scap);
76+
if ($scap === null) {
77+
throw $this->createNotFoundException('The SCAP does not exist');
78+
}
7679
$scap_filename = realpath(__DIR__ . "/../../resources/data/scap/" . $scap->filename);
7780

7881
if (file_exists($scap_filename)) {
@@ -106,6 +109,9 @@ public function scap_view($title, $version, $release): Response
106109
return false;
107110
});
108111
$scap = array_pop($scap);
112+
if ($scap === null) {
113+
throw $this->createNotFoundException('The SCAP does not exist');
114+
}
109115
$scap_filename = realpath(__DIR__ . "/../../resources/data/scap/" . $scap->filename);
110116
if ($scap_filename === false || !is_file($scap_filename)) {
111117
throw $this->createNotFoundException('The SCAP does not exist');

cyber.trackr.live/src/Controller/StigController.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@ public function stig_compare($title, $v1, $r1, $v2, $r2): Response
213213
return false;
214214
});
215215
$stig1 = array_pop($stig1);
216+
if ($stig1 === null) {
217+
throw $this->createNotFoundException('The STIG does not exist');
218+
}
216219
$stig1_filename = realpath(__DIR__ . "/../../resources/data/stig/" . $stig1->filename);
217220
if ($stig1_filename === false || !is_file($stig1_filename)) {
218221
throw $this->createNotFoundException('The STIG does not exist');
@@ -229,6 +232,9 @@ public function stig_compare($title, $v1, $r1, $v2, $r2): Response
229232
return false;
230233
});
231234
$stig2 = array_pop($stig2);
235+
if ($stig2 === null) {
236+
throw $this->createNotFoundException('The STIG does not exist');
237+
}
232238
$stig2_filename = realpath(__DIR__ . "/../../resources/data/stig/" . $stig2->filename);
233239
if ($stig2_filename === false || !is_file($stig2_filename)) {
234240
throw $this->createNotFoundException('The STIG does not exist');
@@ -285,6 +291,9 @@ public function stig_view($title, $version, $release, StigDigestBuilder $digestB
285291
return false;
286292
});
287293
$stig = array_pop($stig);
294+
if ($stig === null) {
295+
throw $this->createNotFoundException('The STIG does not exist');
296+
}
288297
$stig_filename = realpath(__DIR__ . "/../../resources/data/stig/" . $stig->filename);
289298
if ($stig_filename === false || !is_file($stig_filename)) {
290299
throw $this->createNotFoundException('The STIG does not exist');
@@ -435,6 +444,9 @@ public function stig_download($title, $version, $release): Response
435444
});
436445

437446
$stig = array_pop($stig);
447+
if ($stig === null) {
448+
throw $this->createNotFoundException('The STIG does not exist');
449+
}
438450
$stig_filename = realpath(__DIR__ . "/../../resources/data/stig/" . $stig->filename);
439451

440452
if (file_exists($stig_filename)) {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
namespace App\Tests\Controller;
4+
5+
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
6+
7+
class ApiControllerTest extends WebTestCase
8+
{
9+
// A STIG known to exist in stig_toc.json (V4R8). The wizard test covers V6R4.
10+
private const STIG_TITLE = 'Application_Security_and_Development';
11+
private const STIG_VERSION = '4';
12+
private const STIG_RELEASE = '8';
13+
private const STIG_VULN = 'V-69239';
14+
15+
public function testApiSummaryRenders(): void
16+
{
17+
$client = static::createClient();
18+
$client->request('GET', '/api');
19+
$this->assertResponseIsSuccessful();
20+
}
21+
22+
public function testCciListReturnsJson(): void
23+
{
24+
$client = static::createClient();
25+
$client->request('GET', '/api/cci');
26+
27+
$this->assertResponseIsSuccessful();
28+
$data = json_decode((string) $client->getResponse()->getContent(), true);
29+
$this->assertIsArray($data);
30+
$this->assertNotEmpty($data);
31+
}
32+
33+
public function testCciShowReturnsControl(): void
34+
{
35+
$client = static::createClient();
36+
$client->request('GET', '/api/cci/CCI-000001');
37+
38+
$this->assertResponseIsSuccessful();
39+
$data = json_decode((string) $client->getResponse()->getContent(), true);
40+
$this->assertSame('CCI-000001', $data['cci'] ?? null);
41+
}
42+
43+
public function testRmfV4ListReturnsControls(): void
44+
{
45+
$client = static::createClient();
46+
$client->request('GET', '/api/rmf/4');
47+
48+
$this->assertResponseIsSuccessful();
49+
$data = json_decode((string) $client->getResponse()->getContent(), true);
50+
$this->assertArrayHasKey('controls', $data);
51+
$this->assertNotEmpty($data['controls']);
52+
}
53+
54+
public function testRmfV5ListReturnsControls(): void
55+
{
56+
$client = static::createClient();
57+
$client->request('GET', '/api/rmf/5');
58+
59+
$this->assertResponseIsSuccessful();
60+
$data = json_decode((string) $client->getResponse()->getContent(), true);
61+
$this->assertArrayHasKey('controls', $data);
62+
$this->assertNotEmpty($data['controls']);
63+
}
64+
65+
public function testStigListReturnsJson(): void
66+
{
67+
$client = static::createClient();
68+
$client->request('GET', '/api/stig');
69+
70+
$this->assertResponseIsSuccessful();
71+
$data = json_decode((string) $client->getResponse()->getContent(), true);
72+
$this->assertIsArray($data);
73+
$this->assertNotEmpty($data);
74+
}
75+
76+
public function testStigSummaryReturnsJson(): void
77+
{
78+
$client = static::createClient();
79+
$client->request('GET', sprintf(
80+
'/api/stig/%s/%s/%s',
81+
self::STIG_TITLE,
82+
self::STIG_VERSION,
83+
self::STIG_RELEASE
84+
));
85+
86+
$this->assertResponseIsSuccessful();
87+
}
88+
89+
public function testStigVulnReturnsJson(): void
90+
{
91+
$client = static::createClient();
92+
$client->request('GET', sprintf(
93+
'/api/stig/%s/%s/%s/%s',
94+
self::STIG_TITLE,
95+
self::STIG_VERSION,
96+
self::STIG_RELEASE,
97+
self::STIG_VULN
98+
));
99+
100+
$this->assertResponseIsSuccessful();
101+
}
102+
103+
// Exercises the guard added in commit 8710c31: a valid title with an unknown
104+
// version/release resolves to no STIG file and must 404, not fatal with 500.
105+
public function testStigSummaryUnknownVersionIs404(): void
106+
{
107+
$client = static::createClient();
108+
$client->request('GET', sprintf('/api/stig/%s/99/99', self::STIG_TITLE));
109+
$this->assertResponseStatusCodeSame(404);
110+
}
111+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace App\Tests\Controller;
4+
5+
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
6+
7+
class StigControllerTest extends WebTestCase
8+
{
9+
private const TITLE = 'Application_Security_and_Development';
10+
11+
public function testStigIndexRenders(): void
12+
{
13+
$client = static::createClient();
14+
$client->request('GET', '/stig');
15+
$this->assertResponseIsSuccessful();
16+
}
17+
18+
public function testStigViewRenders(): void
19+
{
20+
$client = static::createClient();
21+
$client->request('GET', '/stig/' . self::TITLE . '/4/8');
22+
$this->assertResponseIsSuccessful();
23+
}
24+
25+
public function testStigCompareRenders(): void
26+
{
27+
$client = static::createClient();
28+
// Compare V4R8 against V6R4 — both present in the toc for this title.
29+
$client->request('GET', '/stig/' . self::TITLE . '/4/8/6/4');
30+
$this->assertResponseIsSuccessful();
31+
}
32+
33+
// Guard from commit 8710c31: valid title, unknown version/release -> 404.
34+
public function testStigViewUnknownVersionIs404(): void
35+
{
36+
$client = static::createClient();
37+
$client->request('GET', '/stig/' . self::TITLE . '/99/99');
38+
$this->assertResponseStatusCodeSame(404);
39+
}
40+
}

0 commit comments

Comments
 (0)