💭 Minji's Archive

[KT Cloud TechUp] expoid_db 데이터 시각화 대시보드 제작

October 21, 2025

우선은 phpmyadmin에서 테이블을 제작하고 간단한 테스트 데이터들을 넣어두었다.

[php란?]

  • 오픈소스 언어로 데이터베이스와 연동이 간편한 언어이다.
    <?php
    echo "Hello, World!";
    $name = "홍길동";
    echo "안녕하세요," . $name . "님!";
    

//html과 결합 ?>

의 페이지

```

  • 주요 용도: 웹 개발 (동적 웹페이지 생성), CMS (WordPress, Drupal 등), 웹 애플리케이션 (쇼핑몰, 게시판 등), API 개발 (RESTful API 구축)
<?php
// visualize_exploits.php
// Usage: configure MySQL credentials below and place this file on a PHP-enabled webserver.
// Expected MySQL table structure (example):
// CREATE TABLE exploits (
//   id INT AUTO_INCREMENT PRIMARY KEY,
//   date VARCHAR(50),
//   url TEXT,
//   title TEXT,
//   access_type VARCHAR(100),
//   platform VARCHAR(100),
//   author VARCHAR(100),
//   cve VARCHAR(100)
// );

// ---------- Configuration: edit these ----------
$dbHost = '127.0.0.1';
$dbName = 'mysql_251021';
$dbUser = 'root';
$dbPass = '';
$dbTable = 'exploitdb'; // table name
$perPage = 100; // rows per page for pagination
// -----------------------------------------------

// PDO connection
$dsn = "mysql:host={$dbHost};dbname={$dbName};charset=utf8mb4";
try {
    $pdo = new PDO($dsn, $dbUser, $dbPass, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);
} catch (Exception $e) {
    echo "<h2>DB 연결 실패:</h2>" . htmlspecialchars($e->getMessage());
    exit;
}

// paging
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$offset = ($page - 1) * $perPage;

// basic search filter
$q = isset($_GET['q']) ? trim($_GET['q']) : '';
$params = [];
$where = '';
if ($q !== '') {
    // search in title, author, cve, platform
    $where = "WHERE title LIKE :q OR author LIKE :q OR cve LIKE :q OR platform LIKE :q";
    $params[':q'] = "%{$q}%";
}

// count total
$countSql = "SELECT COUNT(*) AS cnt FROM `{$dbTable}` {$where}";
$stmt = $pdo->prepare($countSql);
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$totalPages = max(1, (int)ceil($total / $perPage));

// fetch rows
$sql = "SELECT date, url, title, access_type, platform, author, cve
        FROM `{$dbTable}` {$where}
        ORDER BY date DESC
        LIMIT :limit OFFSET :offset";
$stmt = $pdo->prepare($sql);
foreach ($params as $k => $v) $stmt->bindValue($k, $v, PDO::PARAM_STR);
$stmt->bindValue(':limit', (int)$perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', (int)$offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll();

// aggregates for charts
$byPlatform = [];
$byAuthor = [];
$byYear = [];
foreach ($rows as $r) {
    $p = $r['platform'] ?: 'Unknown';
    $byPlatform[$p] = ($byPlatform[$p] ?? 0) + 1;

    $a = $r['author'] ?: 'Unknown';
    $byAuthor[$a] = ($byAuthor[$a] ?? 0) + 1;

    $year = 'Unknown';
    if (preg_match('/(\d{4})/', $r['date'], $m)) {
        $year = $m[1];
    } else {
        $ts = strtotime($r['date']);
        if ($ts) $year = date('Y', $ts);
    }
    $byYear[$year] = ($byYear[$year] ?? 0) + 1;
}

arsort($byPlatform);
arsort($byAuthor);
ksort($byYear);

function h($s) { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
?>
<!doctype html>
<html lang="ko">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Exploit DB 시각화 (MySQL)</title>
    <style>
        body{font-family:Inter,system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; padding:20px;}
        table{border-collapse:collapse;width:100%;}
        th,td{border:1px solid #ddd;padding:8px;text-align:left;font-size:13px}
        th{background:#f3f4f6}
        .grid{display:grid;grid-template-columns:1fr 420px;gap:20px}
        .card{background:#fff;border:1px solid #e5e7eb;padding:12px;border-radius:8px}
        .controls{margin-bottom:12px}
        input[type=search]{padding:8px;width:100%;box-sizing:border-box}
        .small{font-size:12px;color:#6b7280}
        .pager{margin-top:8px}
        .pager a{margin-right:6px;text-decoration:none}
        @media(max-width:900px){.grid{grid-template-columns:1fr}}
    </style>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
    <h1>Exploit 데이터 (MySQL) 시각화</h1>
    <p class="small">전체 레코드: <strong><?php echo $total; ?></strong></p>

    <form method="get" class="controls" style="max-width:600px">
        <input type="search" name="q" placeholder="검색 (제목/저자/CVE/플랫폼)" value="<?php echo h($q); ?>">
        <button type="submit">검색</button>
    </form>

    <div class="grid">
        <div>
            <div class="card" id="tableCard">
                <table id="dataTable">
                    <thead>
                        <tr>
                            <th>날짜</th>
                            <th>제목</th>
                            <th>접근</th>
                            <th>플랫폼</th>
                            <th>저자</th>
                            <th>CVE</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($rows as $r): ?>
                        <tr data-url="<?php echo h($r['url']); ?>">
                            <td><?php echo h($r['date']); ?></td>
                            <td><?php echo h($r['title']); ?></td>
                            <td><?php echo h($r['access_type']); ?></td>
                            <td><?php echo h($r['platform']); ?></td>
                            <td><?php echo h($r['author']); ?></td>
                            <td><?php echo h($r['cve']); ?></td>
                        </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>

                <div class="pager">
                    <?php if ($page > 1): ?>
                        <a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $page - 1])); ?>">&laquo; 이전</a>
                    <?php endif; ?>
                    <span>페이지 <?php echo $page; ?> / <?php echo $totalPages; ?></span>
                    <?php if ($page < $totalPages): ?>
                        <a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $page + 1])); ?>">다음 &raquo;</a>
                    <?php endif; ?>
                </div>
            </div>
        </div>

        <div>
            <div class="card">
                <h3>플랫폼 분포</h3>
                <canvas id="platformChart" width="400" height="300"></canvas>
            </div>

            <div class="card" style="margin-top:12px">
                <h3>저자 상위</h3>
                <canvas id="authorChart" width="400" height="300"></canvas>
            </div>

            <div class="card" style="margin-top:12px">
                <h3>연도별 발생 수</h3>
                <canvas id="yearChart" width="400" height="200"></canvas>
            </div>
        </div>
    </div>

    <script>
        const byPlatform = <?php echo json_encode(array_values($byPlatform)); ?>;
        const byPlatformLabels = <?php echo json_encode(array_keys($byPlatform)); ?>;

        const byAuthor = <?php echo json_encode(array_values(array_slice($byAuthor,0,10))); ?>;
        const byAuthorLabels = <?php echo json_encode(array_keys(array_slice($byAuthor,0,10))); ?>;

        const byYearLabels = <?php echo json_encode(array_keys($byYear)); ?>;
        const byYear = <?php echo json_encode(array_values($byYear)); ?>;

        new Chart(document.getElementById('platformChart'), {
            type: 'pie',
            data: { labels: byPlatformLabels, datasets: [{ data: byPlatform }] },
            options: {responsive:true}
        });

        new Chart(document.getElementById('authorChart'), {
            type: 'bar',
            data: { labels: byAuthorLabels, datasets: [{label:'발생 수', data: byAuthor}] },
            options: {responsive:true, scales:{y:{beginAtZero:true}}}
        });

        new Chart(document.getElementById('yearChart'), {
            type: 'line',
            data: { labels: byYearLabels, datasets: [{label:'건수', data: byYear, fill:false, tension:0.2}] },
            options: {responsive:true, scales:{y:{beginAtZero:true}}}
        });

        document.querySelectorAll('#dataTable tbody tr').forEach(tr => {
            tr.addEventListener('click', () => {
                const url = tr.dataset.url;
                if (url) window.open(url, '_blank');
            });
        });
    </script>

    <footer style="margin-top:18px;font-size:12px;color:#6b7280">
        MySQL에서 직접 데이터를 읽어 표와 차트를 생성합니다. DB 칼럼이나 테이블 이름이 다르면 상단 설정을 수정해주세요.
    </footer>
</body>
</html>

뼈대 코드는 위와 같다.

접속까지 완료했다.

db 연결을 해주자

짠~

어제 만든 크롤링 코드와 연동까지 완료했다. exploid-db 사이트에 있는 취약점을 긁어와서 시각화까지 해주는 대시보드 제작 완료.

https://github.com/aneomagig/exploitdb_crawler_visualizer