Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions assessments/_validate.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ function assessment_collect_input(): array
}
}

/* Bir 'review', inherent skoru mevcut residual skorun ALTINA
indiremez: kalıcı tabloda residual > inherent kalır ve tüm etkin
skor hesapları (COALESCE(residual, inherent)) riskin kendisinden
büyük çıkar. Önce residual güncellenmelidir. */
if ($type === 'review' && $risk !== null && $likelihood !== null && $impact !== null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a residual assessment commits after this check reads a risk without a residual, the review can still lower inherent below the newly committed residual because validation runs outside the transaction and is not repeated. Lock the risk row and re-check this invariant inside the same transaction that inserts the assessment and updates risks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At assessments/_validate.php, line 96:

<comment>When a residual assessment commits after this check reads a risk without a residual, the review can still lower inherent below the newly committed residual because validation runs outside the transaction and is not repeated. Lock the risk row and re-check this invariant inside the same transaction that inserts the assessment and updates `risks`.</comment>

<file context>
@@ -89,6 +89,21 @@ function assessment_collect_input(): array
+       indiremez: kalıcı tabloda residual > inherent kalır ve tüm etkin
+       skor hesapları (COALESCE(residual, inherent)) riskin kendisinden
+       büyük çıkar. Önce residual güncellenmelidir. */
+    if ($type === 'review' && $risk !== null && $likelihood !== null && $impact !== null
+        && $risk['residual_likelihood'] !== null && $risk['residual_impact'] !== null
+        && ($likelihood * $impact) < ((int)$risk['residual_likelihood'] * (int)$risk['residual_impact'])) {
</file context>

&& $risk['residual_likelihood'] !== null && $risk['residual_impact'] !== null
&& ($likelihood * $impact) < ((int)$risk['residual_likelihood'] * (int)$risk['residual_impact'])) {
$errors['impact'] = sprintf(
'Inherent skor (%d) mevcut residual skordan (%d) küçük olamaz: kontroller riski artırmaz. '
. 'Residual skor da düşmeli; önce residual değerlendirmesini güncelleyin.',
$likelihood * $impact,
(int)$risk['residual_likelihood'] * (int)$risk['residual_impact']
);
}

$notes = input('notes');
if ($notes !== null && mb_strlen($notes) > 2000) {
$errors['notes'] = 'Not en fazla 2000 karakter olabilir.';
Expand Down
190 changes: 190 additions & 0 deletions tools/assessment_validation_test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);

/**
* RiskOps - Degerlendirme dogrulama regresyon testi
* Calistirma: php tools/assessment_validation_test.php
*
* Bagimlilik YOK: veritabani sunucusu gerekmez. GERCEK uretim
* dogrulayicisi (assessments/_validate.php -> assessment_collect_input)
* calistirilir; db() yalnizca gercek MySQL'in verecegi risk satirini
* saglayan minik bir koza (seam) ile ikame edilir.
*
* REGRESYON ARKAPLANI: 'review' turu degerlendirme bir zamanlar inherent
* skoru mevcut residual skorun ALTINA indirebiliyordu. Kalici tabloda
* residual > inherent kaliyor ve tum etkin skor hesaplari
* (COALESCE(residual, inherent)) riskin kendisinden buyuk cikiyordu.
* Bu invariant risks/_validate.php ve residual tarafiyla korunur;
* review tarafi da ayni sekilde korunmalidir.
*/

if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit('CLI only.');
}

define('RISKOPS_BOOTSTRAPPED', true);

require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../assessments/_validate.php';

/* ---- Minimal db() koza: gercek MySQL'in verecegi risk satiri --------- */

final class AssessmentTestStmt
{
public function __construct(private array|false $row) {}
public function execute(?array $params = null): bool { return true; }
public function fetch(): array|false { return $this->row; }
}

final class AssessmentTestDb
{
public function __construct(private array|false $row) {}
public function prepare(string $sql): AssessmentTestStmt
{
return new AssessmentTestStmt($this->row);
}
}

/** Risk fiksturu: inherent 5x5=25, residual 4x4=16 (gecerli durum). */
function assessment_test_risk(): array
{
return [
'id' => 7, 'risk_code' => 'RISK-2026-0001', 'title' => 'Test riski',
'status' => 'Open',
'likelihood' => 5, 'impact' => 5, 'inherent_score' => 25,
'residual_likelihood' => 4, 'residual_impact' => 4,
];
}

/** GERCEK uretim dogrulayicisini verilen POST ile calistirir. */
function assessment_test_collect(array $post, array|false|null $riskRow = null): array
{
$_POST = $post;
$_GET = [];
global $assessmentTestRiskRow;
$assessmentTestRiskRow = $riskRow ?? assessment_test_risk();
return assessment_collect_input();
}

function db(): AssessmentTestDb
{
global $assessmentTestRiskRow;
return new AssessmentTestDb($assessmentTestRiskRow ?? assessment_test_risk());
}

$PASS = 0;
$FAIL = 0;

function check(string $label, bool $ok, string $detail = ''): void
{
global $PASS, $FAIL;
if ($ok) {
$PASS++;
printf(" [ OK ] %-56s %s\n", $label, $detail);
} else {
$FAIL++;
printf(" [FAIL] %-56s %s\n", $label, $detail);
}
}

function section(string $title): void
{
echo "\n" . str_repeat('-', 72) . "\n " . $title . "\n" . str_repeat('-', 72) . "\n";
}

echo "\n============ Assessment Validation Regression Test =============";

/* ------------------------------------------------------------------ */
section('1) Review inherent skorunu residualin altina indiremez');
/* ------------------------------------------------------------------ */

[, $e] = assessment_test_collect([
'risk_id' => '7', 'assessment_type' => 'review',
'likelihood' => '2', 'impact' => '3',
]);
check('review 2x3=6 vs residual 16 reddedilir', $e !== [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new regression assertions check $e !== [] / $e === [] against the full error map, so the review-lowering rejection can pass whenever any unrelated validation error is present, and the acceptance cases fail if an unrelated error is added to the same path. Pin the assertions to the invariant error (key impact and expected message) so the tests specifically guard the new residual/inherent rule rather than error presence in general.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/assessment_validation_test.php, line 106:

<comment>The new regression assertions check `$e !== []` / `$e === []` against the full error map, so the review-lowering rejection can pass whenever any unrelated validation error is present, and the acceptance cases fail if an unrelated error is added to the same path. Pin the assertions to the invariant error (key `impact` and expected message) so the tests specifically guard the new residual/inherent rule rather than error presence in general.</comment>

<file context>
@@ -0,0 +1,190 @@
+    'risk_id' => '7', 'assessment_type' => 'review',
+    'likelihood' => '2', 'impact' => '3',
+]);
+check('review 2x3=6 vs residual 16 reddedilir', $e !== [],
+    $e === [] ? 'hata donmedi; residual > inherent kalici olurdu' : 'reddedildi');
+
</file context>

$e === [] ? 'hata donmedi; residual > inherent kalici olurdu' : 'reddedildi');

[, $e] = assessment_test_collect([
'risk_id' => '7', 'assessment_type' => 'review',
'likelihood' => '4', 'impact' => '4',
]);
check('review residuala esit (16 == 16) kabul edilir', $e === []);

[, $e] = assessment_test_collect([
'risk_id' => '7', 'assessment_type' => 'review',
'likelihood' => '5', 'impact' => '5',
]);
check('review residualin uzerinde (25) kabul edilir', $e === []);

[, $e] = assessment_test_collect([
'risk_id' => '7', 'assessment_type' => 'review',
'likelihood' => '1', 'impact' => '1',
], array_merge(assessment_test_risk(), [
'residual_likelihood' => null, 'residual_impact' => null,
]));
check('residuali olmayan riskte 1x1 review kabul edilir', $e === []);

/* ------------------------------------------------------------------ */
section('2) Mevcut korumalar yerinde kalmali');
/* ------------------------------------------------------------------ */

$smallRisk = array_merge(assessment_test_risk(), [
'likelihood' => 3, 'impact' => 3, 'inherent_score' => 9,
'residual_likelihood' => 2, 'residual_impact' => 2,
]);

[, $e] = assessment_test_collect([
'risk_id' => '7', 'assessment_type' => 'residual',
'likelihood' => '4', 'impact' => '4',
], $smallRisk);
check('residual 16 > inherent 9 reddedilir', $e !== []);

[, $e] = assessment_test_collect([
'risk_id' => '7', 'assessment_type' => 'residual',
'likelihood' => '3', 'impact' => '3',
], $smallRisk);
check('residual == inherent kabul edilir (sinir)', $e === []);

[, $e] = assessment_test_collect([
'risk_id' => '7', 'assessment_type' => 'initial',
'likelihood' => '1', 'impact' => '1',
]);
check("tip 'initial' elle girilemez", $e !== []);

[, $e] = assessment_test_collect([
'risk_id' => '7', 'assessment_type' => 'hacked',
'likelihood' => '1', 'impact' => '1',
]);
check('gecersiz tip reddedilir', $e !== []);

[, $e] = assessment_test_collect([
'risk_id' => '7', 'assessment_type' => 'review',
'likelihood' => '7', 'impact' => '1',
]);
check('olasilik 1-5 disi reddedilir', $e !== []);

[, $e] = assessment_test_collect([
'risk_id' => '7', 'assessment_type' => 'review',
'likelihood' => '2', 'impact' => '3',
'assessed_at' => date('Y-m-d', strtotime('+1 day')),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When this script starts immediately before midnight, the future-date case can become today's date before validation and fail spuriously. Generate a date at least two days ahead or freeze the clock for this assertion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/assessment_validation_test.php, line 171:

<comment>When this script starts immediately before midnight, the future-date case can become today's date before validation and fail spuriously. Generate a date at least two days ahead or freeze the clock for this assertion.</comment>

<file context>
@@ -0,0 +1,190 @@
+[, $e] = assessment_test_collect([
+    'risk_id' => '7', 'assessment_type' => 'review',
+    'likelihood' => '2', 'impact' => '3',
+    'assessed_at' => date('Y-m-d', strtotime('+1 day')),
+]);
+check('gelecek tarihli assessed_at reddedilir', isset($e['assessed_at']));
</file context>
Suggested change
'assessed_at' => date('Y-m-d', strtotime('+1 day')),
'assessed_at' => date('Y-m-d', strtotime('+2 days')),

]);
check('gelecek tarihli assessed_at reddedilir', isset($e['assessed_at']));

[, $e] = assessment_test_collect([
'risk_id' => '999', 'assessment_type' => 'review',
'likelihood' => '2', 'impact' => '3',
], false);
check('bulunmayan risk reddedilir', $e !== []);

/* ------------------------------------------------------------------ */

echo "\n" . str_repeat('-', 72) . "\n";
printf("Sonuc: %d OK, %d FAIL\n", $PASS, $FAIL);
if ($FAIL > 0) {
echo "ASSESSMENT VALIDATION TEST: FAIL\n";
exit(1);
}
echo "ASSESSMENT VALIDATION TEST: PASS\n";
exit(0);