4cc3f5bee4
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Release configuration (pull_request) Successful in 6s
Generic: Repo Health / Scripts governance (pull_request) Successful in 6s
Generic: Repo Health / Repository health (push) Successful in 14s
Generic: Repo Health / Repository health (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 49s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been skipped
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been skipped
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
- Convert tabs to spaces (3,413 violations) - Fix line endings, trailing whitespace, brace placement - Break lines exceeding 150-char absolute limit - Replace heredoc tab closers with spaces - Fix empty elseif, forbidden function calls - Update phpcs.xml: exclude rules inappropriate for CLI scripts (SideEffects, MissingNamespace, MultipleClasses, HeaderOrder, empty catch blocks) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
417 lines
12 KiB
PHP
417 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* This file is part of a Moko Consulting project.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: MokoStandards.Enterprise.Security
|
|
* INGROUP: MokoStandards.Enterprise
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /lib/Enterprise/SecurityValidator.php
|
|
* BRIEF: Security validation library
|
|
*/
|
|
|
|
/**
|
|
* Security Validator for MokoStandards
|
|
*
|
|
* Provides security scanning and validation:
|
|
* - Credential detection in code/config files
|
|
* - Vulnerability pattern checking
|
|
* - Security best practices validation
|
|
* - Dangerous function detection
|
|
* - File permission validation
|
|
* - Secret management guidance
|
|
*
|
|
* Example usage:
|
|
* ```php
|
|
* $validator = new SecurityValidator();
|
|
* $findings = $validator->scanFile('config.php');
|
|
*
|
|
* if ($validator->hasCriticalFindings()) {
|
|
* $validator->printReport();
|
|
* exit(1);
|
|
* }
|
|
*
|
|
* // Scan entire directory
|
|
* $validator->scanDirectory('src/', ['.php', '.js']);
|
|
* ```
|
|
*
|
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* @package MokoStandards\Enterprise
|
|
* @version 04.00.04
|
|
* @author MokoStandards Team
|
|
* @license GPL-3.0-or-later
|
|
*/
|
|
|
|
namespace MokoEnterprise;
|
|
|
|
use Exception;
|
|
use RecursiveDirectoryIterator;
|
|
use RecursiveIteratorIterator;
|
|
|
|
/**
|
|
* Exception raised when security violations are detected
|
|
*/
|
|
class SecurityViolation extends Exception
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Security validator for detecting vulnerabilities
|
|
*/
|
|
class SecurityValidator
|
|
{
|
|
private const VERSION = '04.06.00';
|
|
|
|
/**
|
|
* Common patterns for credentials and secrets
|
|
*/
|
|
private const CREDENTIAL_PATTERNS = [
|
|
['/password\s*=\s*["\']([^"\']+)["\']/i', 'hardcoded password'],
|
|
['/api[_-]?key\s*=\s*["\']([^"\']+)["\']/i', 'hardcoded API key'],
|
|
['/secret[_-]?key\s*=\s*["\']([^"\']+)["\']/i', 'hardcoded secret key'],
|
|
['/token\s*=\s*["\']([^"\']+)["\']/i', 'hardcoded token'],
|
|
['/aws[_-]?access[_-]?key[_-]?id\s*=\s*["\']([^"\']+)["\']/i', 'AWS access key'],
|
|
['/private[_-]?key\s*=\s*["\']([^"\']+)["\']/i', 'private key'],
|
|
['/["\'][A-Za-z0-9\/+]{40,}["\']/i', 'potential secret (base64)'],
|
|
];
|
|
|
|
/**
|
|
* Dangerous function calls
|
|
*/
|
|
private const DANGEROUS_FUNCTIONS = [
|
|
'eval',
|
|
'exec',
|
|
'system',
|
|
'passthru',
|
|
'shell_exec',
|
|
'assert',
|
|
'create_function',
|
|
'unserialize',
|
|
'extract',
|
|
'$$',
|
|
];
|
|
|
|
/**
|
|
* File permissions that are too permissive
|
|
*/
|
|
private const DANGEROUS_PERMISSIONS = [
|
|
0777, // rwxrwxrwx
|
|
0666, // rw-rw-rw-
|
|
];
|
|
|
|
private array $findings = [];
|
|
|
|
/**
|
|
* Scan a file for security issues
|
|
*
|
|
* @param string $filePath Path to file to scan
|
|
* @param bool $checkCredentials Check for hardcoded credentials
|
|
* @param bool $checkDangerousFunctions Check for dangerous function usage
|
|
* @return array<int, array<string, mixed>> List of security findings
|
|
*/
|
|
public function scanFile(
|
|
string $filePath,
|
|
bool $checkCredentials = true,
|
|
bool $checkDangerousFunctions = true
|
|
): array {
|
|
$findings = [];
|
|
|
|
if (!file_exists($filePath)) {
|
|
return $findings;
|
|
}
|
|
|
|
try {
|
|
$content = file_get_contents($filePath);
|
|
|
|
if ($checkCredentials) {
|
|
$credFindings = $this->checkCredentialsInText($content, $filePath);
|
|
$findings = array_merge($findings, $credFindings);
|
|
}
|
|
|
|
if ($checkDangerousFunctions) {
|
|
$funcFindings = $this->checkDangerousFunctions($content, $filePath);
|
|
$findings = array_merge($findings, $funcFindings);
|
|
}
|
|
} catch (Exception $e) {
|
|
$findings[] = [
|
|
'severity' => 'warning',
|
|
'type' => 'scan_error',
|
|
'file' => $filePath,
|
|
'message' => 'Failed to scan file: ' . $e->getMessage()
|
|
];
|
|
}
|
|
|
|
$this->findings = array_merge($this->findings, $findings);
|
|
return $findings;
|
|
}
|
|
|
|
/**
|
|
* Check for hardcoded credentials in text
|
|
*
|
|
* @param string $text Text to scan
|
|
* @param string $source Source file/location
|
|
* @return array<int, array<string, mixed>> List of findings
|
|
*/
|
|
private function checkCredentialsInText(string $text, string $source): array
|
|
{
|
|
$findings = [];
|
|
|
|
foreach (self::CREDENTIAL_PATTERNS as [$pattern, $description]) {
|
|
if (preg_match_all($pattern, $text, $matches, PREG_OFFSET_CAPTURE)) {
|
|
foreach ($matches[0] as $match) {
|
|
$matchedValue = isset($matches[1]) && !empty($matches[1]) ? $matches[1][0][0] : $match[0];
|
|
|
|
if ($this->isPlaceholder($matchedValue)) {
|
|
continue;
|
|
}
|
|
|
|
$line = substr_count(substr($text, 0, $match[1]), "\n") + 1;
|
|
$snippet = substr($match[0], 0, 50);
|
|
|
|
$findings[] = [
|
|
'severity' => 'high',
|
|
'type' => 'credential',
|
|
'file' => $source,
|
|
'description' => $description,
|
|
'line' => $line,
|
|
'snippet' => $snippet
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $findings;
|
|
}
|
|
|
|
/**
|
|
* Check for dangerous function usage
|
|
*
|
|
* @param string $text Text to scan
|
|
* @param string $source Source file/location
|
|
* @return array<int, array<string, mixed>> List of findings
|
|
*/
|
|
private function checkDangerousFunctions(string $text, string $source): array
|
|
{
|
|
$findings = [];
|
|
|
|
foreach (self::DANGEROUS_FUNCTIONS as $funcName) {
|
|
$pattern = '/\b' . preg_quote($funcName, '/') . '\s*\(/';
|
|
if (preg_match_all($pattern, $text, $matches, PREG_OFFSET_CAPTURE)) {
|
|
foreach ($matches[0] as $match) {
|
|
$line = substr_count(substr($text, 0, $match[1]), "\n") + 1;
|
|
|
|
$findings[] = [
|
|
'severity' => 'medium',
|
|
'type' => 'dangerous_function',
|
|
'file' => $source,
|
|
'function' => $funcName,
|
|
'line' => $line,
|
|
'message' => "Potentially dangerous function: {$funcName}"
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $findings;
|
|
}
|
|
|
|
/**
|
|
* Check if a value looks like a placeholder
|
|
*
|
|
* @param string $value Value to check
|
|
* @return bool True if looks like placeholder
|
|
*/
|
|
private function isPlaceholder(string $value): bool
|
|
{
|
|
$placeholders = [
|
|
'your_', 'example', 'placeholder', 'xxx', 'test',
|
|
'dummy', 'sample', 'replace', 'changeme', 'todo'
|
|
];
|
|
|
|
$valueLower = strtolower($value);
|
|
foreach ($placeholders as $placeholder) {
|
|
if (strpos($valueLower, $placeholder) !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check file permissions for security issues
|
|
*
|
|
* @param string $filePath Path to file
|
|
* @return array<string, mixed>|null Finding if permissions are too permissive, null otherwise
|
|
*/
|
|
public function checkFilePermissions(string $filePath): ?array
|
|
{
|
|
if (!file_exists($filePath)) {
|
|
return null;
|
|
}
|
|
|
|
$perms = fileperms($filePath) & 0777;
|
|
|
|
if (in_array($perms, self::DANGEROUS_PERMISSIONS, true)) {
|
|
$finding = [
|
|
'severity' => 'medium',
|
|
'type' => 'file_permissions',
|
|
'file' => $filePath,
|
|
'permissions' => decoct($perms),
|
|
'message' => sprintf('File has overly permissive permissions: %o', $perms)
|
|
];
|
|
$this->findings[] = $finding;
|
|
return $finding;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Validate that sensitive data comes from environment variables
|
|
*
|
|
* @param string $varName Environment variable name
|
|
* @return bool True if variable exists
|
|
*/
|
|
public function validateEnvironmentVar(string $varName): bool
|
|
{
|
|
return getenv($varName) !== false;
|
|
}
|
|
|
|
/**
|
|
* Get all security findings
|
|
*
|
|
* @param string|null $severity Filter by severity (high, medium, low, warning)
|
|
* @return array<int, array<string, mixed>> List of findings
|
|
*/
|
|
public function getFindings(?string $severity = null): array
|
|
{
|
|
if ($severity !== null) {
|
|
return array_filter($this->findings, function ($finding) use ($severity) {
|
|
return ($finding['severity'] ?? '') === $severity;
|
|
});
|
|
}
|
|
return $this->findings;
|
|
}
|
|
|
|
/**
|
|
* Check if there are any critical/high severity findings
|
|
*
|
|
* @return bool True if critical findings exist
|
|
*/
|
|
public function hasCriticalFindings(): bool
|
|
{
|
|
foreach ($this->findings as $finding) {
|
|
if (in_array($finding['severity'] ?? '', ['critical', 'high'], true)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Print a security report
|
|
*/
|
|
public function printReport(): void
|
|
{
|
|
echo "\n" . str_repeat('=', 60) . "\n";
|
|
echo "Security Validation Report\n";
|
|
echo str_repeat('=', 60) . "\n";
|
|
|
|
if (empty($this->findings)) {
|
|
echo "\n✓ No security issues found!\n";
|
|
echo str_repeat('=', 60) . "\n\n";
|
|
return;
|
|
}
|
|
|
|
// Group by severity
|
|
$bySeverity = [];
|
|
foreach ($this->findings as $finding) {
|
|
$sev = $finding['severity'] ?? 'unknown';
|
|
if (!isset($bySeverity[$sev])) {
|
|
$bySeverity[$sev] = [];
|
|
}
|
|
$bySeverity[$sev][] = $finding;
|
|
}
|
|
|
|
// Print findings by severity
|
|
foreach (['critical', 'high', 'medium', 'low', 'warning'] as $sev) {
|
|
if (isset($bySeverity[$sev])) {
|
|
echo sprintf("\n%s Severity (%d findings):\n", strtoupper($sev), count($bySeverity[$sev]));
|
|
foreach ($bySeverity[$sev] as $finding) {
|
|
$message = $finding['message'] ?? $finding['description'] ?? 'No description';
|
|
echo " - {$finding['type']}: {$message}\n";
|
|
if (isset($finding['file'])) {
|
|
echo " File: {$finding['file']}\n";
|
|
}
|
|
if (isset($finding['line'])) {
|
|
echo " Line: {$finding['line']}\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$total = count($this->findings);
|
|
$critical = count($bySeverity['critical'] ?? []) + count($bySeverity['high'] ?? []);
|
|
|
|
echo "\nTotal findings: {$total}\n";
|
|
echo "Critical/High: {$critical}\n";
|
|
echo str_repeat('=', 60) . "\n\n";
|
|
}
|
|
|
|
/**
|
|
* Clear all findings
|
|
*/
|
|
public function clearFindings(): void
|
|
{
|
|
$this->findings = [];
|
|
}
|
|
|
|
/**
|
|
* Scan a directory for security issues
|
|
*
|
|
* @param string $directory Directory to scan
|
|
* @param array<int, string>|null $extensions File extensions to scan
|
|
*/
|
|
public function scanDirectory(string $directory, ?array $extensions = null): void
|
|
{
|
|
if ($extensions === null) {
|
|
$extensions = ['.php', '.sh', '.yaml', '.yml', '.json', '.conf', '.cfg'];
|
|
}
|
|
|
|
if (!is_dir($directory)) {
|
|
return;
|
|
}
|
|
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($directory)
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
if ($file->isFile()) {
|
|
$filePath = $file->getPathname();
|
|
foreach ($extensions as $ext) {
|
|
if (substr($filePath, -strlen($ext)) === $ext) {
|
|
$this->scanFile($filePath);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function getVersion(): string
|
|
{
|
|
return self::VERSION;
|
|
}
|
|
}
|