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
10 changes: 5 additions & 5 deletions api/dashboard_charts.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@
/* tek eksende gosterilebilir). */
/* ------------------------------------------------------------------ */

$months = [];
for ($i = 11; $i >= 0; $i--) {
$months[date('Y-m', strtotime("-{$i} month"))] = ['opened' => 0, 'closed' => 0];
}
$since = date('Y-m-01', strtotime('-11 month'));
// Takvim ayi penceresi ayin 1'ine sabitlenerek kurulur (bkz. recent_months):
// strtotime('-N month') ay sonu gunlerinde bir sonraki aya tasiyordu.
$monthKeys = recent_months(12);
$months = array_fill_keys($monthKeys, ['opened' => 0, 'closed' => 0]);
$since = $monthKeys[0] . '-01';

$rows = $pdo->prepare(
"SELECT DATE_FORMAT(created_at, '%Y-%m') AS ay, COUNT(*) AS adet
Expand Down
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
&& $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
30 changes: 30 additions & 0 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,36 @@ function days_until(?string $date): ?int
return (int)floor(($t - strtotime(date('Y-m-d'))) / 86400);
}

/**
* Son $count takvim ayinin 'Y-m' anahtarlarini eskiden yeniye sirali dondurur.
* Pencerenin alt siniri (SQL :since degeri) ilk anahtarin '-01' ekidir.
*
* NEDEN AYRI BIR FONKSIYON: strtotime('-N month') gun tasmasini kirmaz;
* ayin 29-31'inde hedef ayda o gun yoksa sonuc bir SONRAKI aya taser.
* Dashboard trend penceresi (api/dashboard_charts.php) bu yuzden ay sonu
* isteklerinde 12 yerine 7-11 ay uretiyor, kayip aylarin riskleri hic
* sayilmiyordu. Burada ay aritmetigi her zaman ayin 1'ine sabitlenir;
* 1. gunden cikarilan ay asla tasmaz.
*
* @param int $count Kac ay dondurecek (1 = yalnizca bulunulan ay).
* @param int|null $baseTs Pencerenin bittigi an (null = simdi).
* @return list<string> 'YYYY-MM' anahtarlari, eskiden yeniye.
*/
function recent_months(int $count, ?int $baseTs = null): array
{
if ($count < 1) {
return [];
}
$anchor = (new DateTimeImmutable(date('Y-m-d H:i:s', $baseTs ?? time())))
->modify('first day of this month midnight');

$out = [];
for ($i = $count - 1; $i >= 0; $i--) {
$out[] = $anchor->modify("-{$i} months")->format('Y-m');
}
return $out;
}

function str_limit(?string $text, int $limit = 80): string
{
$text = trim((string)$text);
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 !== [],
$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')),
]);
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);
Loading