feat: ajout commande app:dns:check pour verifier la configuration DNS
src/Command/CheckDnsCommand.php (nouveau fichier): - Commande Symfony app:dns:check qui verifie les DNS de siteconseil.fr et esy-web.dev - Verification SPF: presence des includes amazonses.com et mail.esy-web.dev, terminaison -all ou ~all - Verification DMARC: presence sur _dmarc.*, politique p=reject/quarantine/none, presence de rua (adresse de rapport) - Verification DKIM: test de 10 selecteurs (ses1/ses2/ses3, default, mail, k1, google, selector1/selector2, dkim) en CNAME et TXT - Verification MX: presence de mail.esy-web.dev comme serveur de reception - Verification Bounce: enregistrements MX/CNAME/TXT sur bounce.*, verification du SPF bounce avec include:amazonses.com - Envoi d'un email de rapport complet a l'admin avec: - Bandeau colore vert (OK) / jaune (warnings) / rouge (erreurs) - Tableau des verifications reussies avec checkmarks verts - Tableau des erreurs en rouge avec croix - Tableau des avertissements en jaune avec triangles - Date du rapport et liste des domaines verifies - Le rapport est envoye dans tous les cas (succes ou echec) - Retourne FAILURE si au moins une erreur est detectee Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
399
src/Command/CheckDnsCommand.php
Normal file
399
src/Command/CheckDnsCommand.php
Normal file
@@ -0,0 +1,399 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\MailerService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:dns:check',
|
||||
description: 'Verifie la configuration DNS (SPF, DMARC, DKIM, MX, bounce) pour siteconseil.fr et esy-web.dev',
|
||||
)]
|
||||
class CheckDnsCommand extends Command
|
||||
{
|
||||
private const DOMAINS = ['siteconseil.fr', 'esy-web.dev'];
|
||||
|
||||
private const EXPECTED_SPF_INCLUDES = ['amazonses.com', 'mail.esy-web.dev'];
|
||||
|
||||
private const EXPECTED_MX = [
|
||||
'siteconseil.fr' => 'mail.esy-web.dev',
|
||||
'esy-web.dev' => 'mail.esy-web.dev',
|
||||
];
|
||||
|
||||
private const DKIM_SELECTORS = ['ses1', 'ses2', 'ses3'];
|
||||
|
||||
public function __construct(
|
||||
private MailerService $mailer,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Verification DNS - CRM SITECONSEIL');
|
||||
|
||||
$errors = [];
|
||||
$warnings = [];
|
||||
$successes = [];
|
||||
|
||||
foreach (self::DOMAINS as $domain) {
|
||||
$io->section('Domaine : '.$domain);
|
||||
|
||||
$this->checkSpf($domain, $io, $errors, $warnings, $successes);
|
||||
$this->checkDmarc($domain, $io, $errors, $successes);
|
||||
$this->checkDkim($domain, $io, $errors, $warnings, $successes);
|
||||
$this->checkMx($domain, $io, $errors, $successes);
|
||||
$this->checkBounce($domain, $io, $errors, $warnings, $successes);
|
||||
}
|
||||
|
||||
$io->newLine();
|
||||
|
||||
$this->sendReportEmail($errors, $warnings, $successes);
|
||||
|
||||
if ([] !== $errors) {
|
||||
$io->error(\count($errors).' erreur(s) detectee(s) :');
|
||||
$io->listing($errors);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if ([] !== $warnings) {
|
||||
$io->warning(\count($warnings).' avertissement(s) :');
|
||||
$io->listing($warnings);
|
||||
}
|
||||
|
||||
$io->success('Tous les enregistrements DNS sont correctement configures. Rapport envoye par email.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $errors
|
||||
* @param list<string> $warnings
|
||||
* @param list<string> $successes
|
||||
*/
|
||||
private function checkSpf(string $domain, SymfonyStyle $io, array &$errors, array &$warnings, array &$successes): void
|
||||
{
|
||||
$records = $this->getTxtRecords($domain);
|
||||
$spf = null;
|
||||
|
||||
foreach ($records as $record) {
|
||||
if (str_starts_with($record, 'v=spf1')) {
|
||||
$spf = $record;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $spf) {
|
||||
$errors[] = "[$domain] SPF : aucun enregistrement SPF trouve";
|
||||
$io->text(' SPF : <fg=red>ABSENT</>');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$io->text(' SPF : '.$spf);
|
||||
|
||||
foreach (self::EXPECTED_SPF_INCLUDES as $include) {
|
||||
if (!str_contains($spf, 'include:'.$include)) {
|
||||
$errors[] = "[$domain] SPF : include:$include manquant dans l'enregistrement SPF";
|
||||
$io->text(' <fg=red>MANQUANT</> include:'.$include);
|
||||
} else {
|
||||
$successes[] = "[$domain] SPF : include:$include present";
|
||||
$io->text(' <fg=green>OK</> include:'.$include);
|
||||
}
|
||||
}
|
||||
|
||||
if (!str_contains($spf, '-all') && !str_contains($spf, '~all')) {
|
||||
$warnings[] = "[$domain] SPF : pas de -all ou ~all en fin d'enregistrement";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $errors
|
||||
* @param list<string> $successes
|
||||
*/
|
||||
private function checkDmarc(string $domain, SymfonyStyle $io, array &$errors, array &$successes): void
|
||||
{
|
||||
$records = $this->getTxtRecords('_dmarc.'.$domain);
|
||||
$dmarc = null;
|
||||
|
||||
foreach ($records as $record) {
|
||||
if (str_starts_with($record, 'v=DMARC1')) {
|
||||
$dmarc = $record;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $dmarc) {
|
||||
$errors[] = "[$domain] DMARC : aucun enregistrement DMARC trouve sur _dmarc.$domain";
|
||||
$io->text(' DMARC : <fg=red>ABSENT</>');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$io->text(' DMARC : '.$dmarc);
|
||||
|
||||
if (str_contains($dmarc, 'p=none')) {
|
||||
$io->text(' <fg=yellow>ATTENTION</> politique p=none (pas de protection)');
|
||||
} elseif (str_contains($dmarc, 'p=quarantine')) {
|
||||
$successes[] = "[$domain] DMARC : politique p=quarantine active";
|
||||
$io->text(' <fg=green>OK</> politique p=quarantine');
|
||||
} elseif (str_contains($dmarc, 'p=reject')) {
|
||||
$successes[] = "[$domain] DMARC : politique p=reject active (stricte)";
|
||||
$io->text(' <fg=green>OK</> politique p=reject (stricte)');
|
||||
}
|
||||
|
||||
if (!str_contains($dmarc, 'rua=')) {
|
||||
$errors[] = "[$domain] DMARC : pas d'adresse de rapport (rua) configuree";
|
||||
} else {
|
||||
$successes[] = "[$domain] DMARC : adresse de rapport (rua) configuree";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $errors
|
||||
* @param list<string> $warnings
|
||||
* @param list<string> $successes
|
||||
*/
|
||||
private function checkDkim(string $domain, SymfonyStyle $io, array &$errors, array &$warnings, array &$successes): void
|
||||
{
|
||||
$found = 0;
|
||||
|
||||
foreach (self::DKIM_SELECTORS as $selector) {
|
||||
$cname = $this->getCnameRecord($selector.'._domainkey.'.$domain);
|
||||
|
||||
if (null !== $cname) {
|
||||
$io->text(' DKIM ('.$selector.') : <fg=green>OK</> → '.$cname);
|
||||
++$found;
|
||||
}
|
||||
}
|
||||
|
||||
// Essayer aussi les selecteurs personnalises courants
|
||||
$customSelectors = ['default', 'mail', 'k1', 'google', 'selector1', 'selector2', 'dkim'];
|
||||
foreach ($customSelectors as $selector) {
|
||||
$cname = $this->getCnameRecord($selector.'._domainkey.'.$domain);
|
||||
$txt = $this->getDkimTxtRecord($selector.'._domainkey.'.$domain);
|
||||
|
||||
if (null !== $cname) {
|
||||
$io->text(' DKIM ('.$selector.') : <fg=green>OK</> CNAME → '.$cname);
|
||||
++$found;
|
||||
} elseif (null !== $txt) {
|
||||
$io->text(' DKIM ('.$selector.') : <fg=green>OK</> TXT');
|
||||
++$found;
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === $found) {
|
||||
$warnings[] = "[$domain] DKIM : aucun enregistrement DKIM trouve (selecteurs testes: ".implode(', ', array_merge(self::DKIM_SELECTORS, $customSelectors)).')';
|
||||
$io->text(' DKIM : <fg=yellow>AUCUN SELECTEUR TROUVE</>');
|
||||
} else {
|
||||
$successes[] = "[$domain] DKIM : $found selecteur(s) DKIM trouve(s)";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $errors
|
||||
* @param list<string> $successes
|
||||
*/
|
||||
private function checkMx(string $domain, SymfonyStyle $io, array &$errors, array &$successes): void
|
||||
{
|
||||
$mxRecords = dns_get_record($domain, \DNS_MX) ?: [];
|
||||
|
||||
if ([] === $mxRecords) {
|
||||
$errors[] = "[$domain] MX : aucun enregistrement MX trouve";
|
||||
$io->text(' MX : <fg=red>ABSENT</>');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$expectedMx = self::EXPECTED_MX[$domain] ?? null;
|
||||
|
||||
foreach ($mxRecords as $mx) {
|
||||
$target = rtrim($mx['target'] ?? '', '.');
|
||||
$priority = $mx['pri'] ?? '?';
|
||||
$isExpected = null !== $expectedMx && str_contains($target, $expectedMx);
|
||||
|
||||
$io->text(' MX : '.($isExpected ? '<fg=green>OK</>' : '<fg=yellow>?</>').
|
||||
' '.$target.' (priorite: '.$priority.')');
|
||||
}
|
||||
|
||||
if (null !== $expectedMx) {
|
||||
$found = false;
|
||||
foreach ($mxRecords as $mx) {
|
||||
if (str_contains(rtrim($mx['target'] ?? '', '.'), $expectedMx)) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
$errors[] = "[$domain] MX : $expectedMx attendu mais non trouve";
|
||||
} else {
|
||||
$successes[] = "[$domain] MX : $expectedMx correctement configure";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $errors
|
||||
* @param list<string> $warnings
|
||||
* @param list<string> $successes
|
||||
*/
|
||||
private function checkBounce(string $domain, SymfonyStyle $io, array &$errors, array &$warnings, array &$successes): void
|
||||
{
|
||||
$bounceDomain = 'bounce.'.$domain;
|
||||
|
||||
// Verifier MX sur bounce.*
|
||||
$mxRecords = dns_get_record($bounceDomain, \DNS_MX) ?: [];
|
||||
|
||||
if ([] !== $mxRecords) {
|
||||
foreach ($mxRecords as $mx) {
|
||||
$io->text(' Bounce MX : <fg=green>OK</> '.rtrim($mx['target'] ?? '', '.').' (priorite: '.($mx['pri'] ?? '?').')');
|
||||
}
|
||||
} else {
|
||||
// Verifier CNAME sur bounce.*
|
||||
$cname = $this->getCnameRecord($bounceDomain);
|
||||
if (null !== $cname) {
|
||||
$io->text(' Bounce CNAME : <fg=green>OK</> → '.$cname);
|
||||
} else {
|
||||
// Verifier TXT sur bounce.*
|
||||
$txt = $this->getTxtRecords($bounceDomain);
|
||||
if ([] !== $txt) {
|
||||
$io->text(' Bounce TXT : <fg=green>OK</> '.implode(' | ', $txt));
|
||||
} else {
|
||||
$warnings[] = "[$domain] Bounce : aucun enregistrement MX/CNAME/TXT sur $bounceDomain";
|
||||
$io->text(' Bounce : <fg=yellow>AUCUN ENREGISTREMENT</> sur '.$bounceDomain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verifier le TXT SPF sur bounce.*
|
||||
$bounceTxt = $this->getTxtRecords($bounceDomain);
|
||||
$bounceSpf = null;
|
||||
foreach ($bounceTxt as $record) {
|
||||
if (str_starts_with($record, 'v=spf1')) {
|
||||
$bounceSpf = $record;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $bounceSpf) {
|
||||
$io->text(' Bounce SPF : '.$bounceSpf);
|
||||
if (!str_contains($bounceSpf, 'amazonses.com')) {
|
||||
$warnings[] = "[$domain] Bounce SPF : include:amazonses.com manquant sur $bounceDomain";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function getTxtRecords(string $domain): array
|
||||
{
|
||||
$records = dns_get_record($domain, \DNS_TXT) ?: [];
|
||||
|
||||
return array_map(fn (array $r) => $r['txt'] ?? '', $records);
|
||||
}
|
||||
|
||||
private function getCnameRecord(string $domain): ?string
|
||||
{
|
||||
$records = @dns_get_record($domain, \DNS_CNAME) ?: [];
|
||||
|
||||
if ([] === $records) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return rtrim($records[0]['target'] ?? '', '.');
|
||||
}
|
||||
|
||||
private function getDkimTxtRecord(string $domain): ?string
|
||||
{
|
||||
$records = $this->getTxtRecords($domain);
|
||||
|
||||
foreach ($records as $record) {
|
||||
if (str_contains($record, 'v=DKIM1') || str_contains($record, 'k=rsa')) {
|
||||
return $record;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $errors
|
||||
* @param list<string> $warnings
|
||||
* @param list<string> $successes
|
||||
*/
|
||||
private function sendReportEmail(array $errors, array $warnings, array $successes): void
|
||||
{
|
||||
$hasErrors = [] !== $errors;
|
||||
$date = (new \DateTimeImmutable())->format('d/m/Y H:i:s');
|
||||
|
||||
if ($hasErrors) {
|
||||
$subject = 'CRM SITECONSEIL - Alerte DNS : '.\count($errors).' erreur(s) detectee(s)';
|
||||
$statusColor = '#dc2626';
|
||||
$statusText = 'ERREURS DETECTEES';
|
||||
} elseif ([] !== $warnings) {
|
||||
$subject = 'CRM SITECONSEIL - DNS : '.\count($warnings).' avertissement(s)';
|
||||
$statusColor = '#f59e0b';
|
||||
$statusText = 'AVERTISSEMENTS';
|
||||
} else {
|
||||
$subject = 'CRM SITECONSEIL - DNS : configuration OK';
|
||||
$statusColor = '#16a34a';
|
||||
$statusText = 'TOUT EST OK';
|
||||
}
|
||||
|
||||
$body = '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">';
|
||||
$body .= '<tr><td style="background-color: '.$statusColor.'; color: #ffffff; padding-top: 12px; padding-bottom: 12px; padding-left: 16px; padding-right: 16px; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; text-align: center;">'.$statusText.'</td></tr>';
|
||||
$body .= '</table>';
|
||||
|
||||
$body .= '<h2 style="font-size: 18px; font-weight: 700; margin-top: 16px; margin-bottom: 8px;">Rapport DNS - '.$date.'</h2>';
|
||||
$body .= '<p style="font-size: 13px; color: #666; margin-top: 0; margin-bottom: 16px;">Domaines verifies : <strong>'.implode(', ', self::DOMAINS).'</strong></p>';
|
||||
|
||||
// Succes
|
||||
if ([] !== $successes) {
|
||||
$body .= '<h3 style="font-size: 14px; font-weight: 700; color: #16a34a; margin-top: 16px; margin-bottom: 8px;">Verifications reussies ('.\count($successes).') :</h3>';
|
||||
$body .= '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border: 1px solid #e5e5e5; margin-bottom: 16px;">';
|
||||
foreach ($successes as $i => $success) {
|
||||
$bg = 0 === $i % 2 ? '#ffffff' : '#f9fafb';
|
||||
$body .= '<tr><td style="padding-top: 6px; padding-bottom: 6px; padding-left: 12px; padding-right: 12px; font-size: 12px; background-color: '.$bg.'; border-bottom: 1px solid #eeeeee;">✓ '.$success.'</td></tr>';
|
||||
}
|
||||
$body .= '</table>';
|
||||
}
|
||||
|
||||
// Erreurs
|
||||
if ($hasErrors) {
|
||||
$body .= '<h3 style="font-size: 14px; font-weight: 700; color: #dc2626; margin-top: 16px; margin-bottom: 8px;">Erreurs ('.\count($errors).') :</h3>';
|
||||
$body .= '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border: 1px solid #fca5a5; margin-bottom: 16px;">';
|
||||
foreach ($errors as $error) {
|
||||
$body .= '<tr><td style="padding-top: 6px; padding-bottom: 6px; padding-left: 12px; padding-right: 12px; font-size: 12px; background-color: #fef2f2; color: #991b1b; border-bottom: 1px solid #fca5a5;">✗ '.$error.'</td></tr>';
|
||||
}
|
||||
$body .= '</table>';
|
||||
}
|
||||
|
||||
// Warnings
|
||||
if ([] !== $warnings) {
|
||||
$body .= '<h3 style="font-size: 14px; font-weight: 700; color: #f59e0b; margin-top: 16px; margin-bottom: 8px;">Avertissements ('.\count($warnings).') :</h3>';
|
||||
$body .= '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border: 1px solid #fde68a; margin-bottom: 16px;">';
|
||||
foreach ($warnings as $warning) {
|
||||
$body .= '<tr><td style="padding-top: 6px; padding-bottom: 6px; padding-left: 12px; padding-right: 12px; font-size: 12px; background-color: #fffbeb; color: #92400e; border-bottom: 1px solid #fde68a;">⚠ '.$warning.'</td></tr>';
|
||||
}
|
||||
$body .= '</table>';
|
||||
}
|
||||
|
||||
$body .= '<p style="font-size: 11px; color: #888; margin-top: 16px;">Rapport genere par la commande app:dns:check le '.$date.'</p>';
|
||||
|
||||
$this->mailer->sendEmail(
|
||||
$this->mailer->getAdminEmail(),
|
||||
$subject,
|
||||
$body,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user