Backend refactor reconciliation + ASPA verification fix + silent-failure audit #2
871
aspa-adoption-history.json
Normal file
871
aspa-adoption-history.json
Normal file
@ -0,0 +1,871 @@
|
||||
[
|
||||
{
|
||||
"date": "2026-04-29",
|
||||
"ts": 1777506539332,
|
||||
"aspa_objects": 1781,
|
||||
"roa_count": 846608,
|
||||
"atlas_asns_total": 4698,
|
||||
"atlas_asns_with_aspa": 393,
|
||||
"coverage_pct_atlas": 8.37,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-04-30",
|
||||
"ts": 1777591207204,
|
||||
"aspa_objects": 1789,
|
||||
"roa_count": 845275,
|
||||
"atlas_asns_total": 4706,
|
||||
"atlas_asns_with_aspa": 391,
|
||||
"coverage_pct_atlas": 8.31,
|
||||
"aspa_delta": 4,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-01",
|
||||
"ts": 1777676486871,
|
||||
"aspa_objects": 1791,
|
||||
"roa_count": 835376,
|
||||
"atlas_asns_total": 4698,
|
||||
"atlas_asns_with_aspa": 390,
|
||||
"coverage_pct_atlas": 8.3,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-02",
|
||||
"ts": 1777764423571,
|
||||
"aspa_objects": 1790,
|
||||
"roa_count": 838422,
|
||||
"atlas_asns_total": 4698,
|
||||
"atlas_asns_with_aspa": 389,
|
||||
"coverage_pct_atlas": 8.28,
|
||||
"aspa_delta": -1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-03",
|
||||
"ts": 1777852360413,
|
||||
"aspa_objects": 1788,
|
||||
"roa_count": 848778,
|
||||
"atlas_asns_total": 4698,
|
||||
"atlas_asns_with_aspa": 391,
|
||||
"coverage_pct_atlas": 8.32,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-04",
|
||||
"ts": 1777925641049,
|
||||
"aspa_objects": 1794,
|
||||
"roa_count": 849251,
|
||||
"atlas_asns_total": 4701,
|
||||
"atlas_asns_with_aspa": 393,
|
||||
"coverage_pct_atlas": 8.36,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-05",
|
||||
"ts": 1778013577684,
|
||||
"aspa_objects": 1803,
|
||||
"roa_count": 849639,
|
||||
"atlas_asns_total": 4706,
|
||||
"atlas_asns_with_aspa": 390,
|
||||
"coverage_pct_atlas": 8.29,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-06",
|
||||
"ts": 1778101514437,
|
||||
"aspa_objects": 1811,
|
||||
"roa_count": 851176,
|
||||
"atlas_asns_total": 4709,
|
||||
"atlas_asns_with_aspa": 392,
|
||||
"coverage_pct_atlas": 8.32,
|
||||
"aspa_delta": 3,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-07",
|
||||
"ts": 1778189451143,
|
||||
"aspa_objects": 1819,
|
||||
"roa_count": 854308,
|
||||
"atlas_asns_total": 4704,
|
||||
"atlas_asns_with_aspa": 397,
|
||||
"coverage_pct_atlas": 8.44,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-08",
|
||||
"ts": 1778277387928,
|
||||
"aspa_objects": 1826,
|
||||
"roa_count": 855093,
|
||||
"atlas_asns_total": 4618,
|
||||
"atlas_asns_with_aspa": 391,
|
||||
"coverage_pct_atlas": 8.47,
|
||||
"aspa_delta": 2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-09",
|
||||
"ts": 1778365324597,
|
||||
"aspa_objects": 1829,
|
||||
"roa_count": 855728,
|
||||
"atlas_asns_total": 4617,
|
||||
"atlas_asns_with_aspa": 386,
|
||||
"coverage_pct_atlas": 8.36,
|
||||
"aspa_delta": 2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-10",
|
||||
"ts": 1778453261304,
|
||||
"aspa_objects": 1830,
|
||||
"roa_count": 855858,
|
||||
"atlas_asns_total": 4630,
|
||||
"atlas_asns_with_aspa": 392,
|
||||
"coverage_pct_atlas": 8.47,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-11",
|
||||
"ts": 1778532854561,
|
||||
"aspa_objects": 1842,
|
||||
"roa_count": 852307,
|
||||
"atlas_asns_total": 4644,
|
||||
"atlas_asns_with_aspa": 396,
|
||||
"coverage_pct_atlas": 8.53,
|
||||
"aspa_delta": 4,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-12",
|
||||
"ts": 1778619510110,
|
||||
"aspa_objects": 1851,
|
||||
"roa_count": 860353,
|
||||
"atlas_asns_total": 4638,
|
||||
"atlas_asns_with_aspa": 400,
|
||||
"coverage_pct_atlas": 8.62,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-13",
|
||||
"ts": 1778713039453,
|
||||
"aspa_objects": 1853,
|
||||
"roa_count": 869026,
|
||||
"atlas_asns_total": 4644,
|
||||
"atlas_asns_with_aspa": 399,
|
||||
"coverage_pct_atlas": 8.59,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-14",
|
||||
"ts": 1778801086379,
|
||||
"aspa_objects": 1874,
|
||||
"roa_count": 870195,
|
||||
"atlas_asns_total": 4641,
|
||||
"atlas_asns_with_aspa": 402,
|
||||
"coverage_pct_atlas": 8.66,
|
||||
"aspa_delta": 5,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-15",
|
||||
"ts": 1778889133380,
|
||||
"aspa_objects": 1881,
|
||||
"roa_count": 871665,
|
||||
"atlas_asns_total": 4656,
|
||||
"atlas_asns_with_aspa": 406,
|
||||
"coverage_pct_atlas": 8.72,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-16",
|
||||
"ts": 1778964126500,
|
||||
"aspa_objects": 1885,
|
||||
"roa_count": 871578,
|
||||
"atlas_asns_total": 4652,
|
||||
"atlas_asns_with_aspa": 407,
|
||||
"coverage_pct_atlas": 8.75,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-17",
|
||||
"ts": 1779051783629,
|
||||
"aspa_objects": 1884,
|
||||
"roa_count": 871644,
|
||||
"atlas_asns_total": 4648,
|
||||
"atlas_asns_with_aspa": 407,
|
||||
"coverage_pct_atlas": 8.76,
|
||||
"aspa_delta": -2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-18",
|
||||
"ts": 1779139440853,
|
||||
"aspa_objects": 1897,
|
||||
"roa_count": 874620,
|
||||
"atlas_asns_total": 4659,
|
||||
"atlas_asns_with_aspa": 408,
|
||||
"coverage_pct_atlas": 8.76,
|
||||
"aspa_delta": 6,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-19",
|
||||
"ts": 1779227098051,
|
||||
"aspa_objects": 1931,
|
||||
"roa_count": 877276,
|
||||
"atlas_asns_total": 4663,
|
||||
"atlas_asns_with_aspa": 409,
|
||||
"coverage_pct_atlas": 8.77,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-20",
|
||||
"ts": 1779314755375,
|
||||
"aspa_objects": 1945,
|
||||
"roa_count": 882413,
|
||||
"atlas_asns_total": 0,
|
||||
"atlas_asns_with_aspa": 0,
|
||||
"coverage_pct_atlas": 0,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-21",
|
||||
"ts": 1779402412305,
|
||||
"aspa_objects": 1956,
|
||||
"roa_count": 886581,
|
||||
"atlas_asns_total": 4657,
|
||||
"atlas_asns_with_aspa": 415,
|
||||
"coverage_pct_atlas": 8.91,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-22",
|
||||
"ts": 1779490069486,
|
||||
"aspa_objects": 1963,
|
||||
"roa_count": 886989,
|
||||
"atlas_asns_total": 4656,
|
||||
"atlas_asns_with_aspa": 416,
|
||||
"coverage_pct_atlas": 8.93,
|
||||
"aspa_delta": -1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-23",
|
||||
"ts": 1779577726657,
|
||||
"aspa_objects": 1966,
|
||||
"roa_count": 887123,
|
||||
"atlas_asns_total": 4662,
|
||||
"atlas_asns_with_aspa": 421,
|
||||
"coverage_pct_atlas": 9.03,
|
||||
"aspa_delta": -1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-24",
|
||||
"ts": 1779665383794,
|
||||
"aspa_objects": 1973,
|
||||
"roa_count": 887505,
|
||||
"atlas_asns_total": 4658,
|
||||
"atlas_asns_with_aspa": 419,
|
||||
"coverage_pct_atlas": 9,
|
||||
"aspa_delta": 3,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-25",
|
||||
"ts": 1779753040747,
|
||||
"aspa_objects": 1984,
|
||||
"roa_count": 889453,
|
||||
"atlas_asns_total": 4657,
|
||||
"atlas_asns_with_aspa": 427,
|
||||
"coverage_pct_atlas": 9.17,
|
||||
"aspa_delta": 6,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-26",
|
||||
"ts": 1779826088383,
|
||||
"aspa_objects": 1993,
|
||||
"roa_count": 896728,
|
||||
"atlas_asns_total": 4683,
|
||||
"atlas_asns_with_aspa": 426,
|
||||
"coverage_pct_atlas": 9.1,
|
||||
"aspa_delta": 8,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-27",
|
||||
"ts": 1779924276466,
|
||||
"aspa_objects": 2001,
|
||||
"roa_count": 904935,
|
||||
"atlas_asns_total": 4673,
|
||||
"atlas_asns_with_aspa": 426,
|
||||
"coverage_pct_atlas": 9.12,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-28",
|
||||
"ts": 1780012471849,
|
||||
"aspa_objects": 2014,
|
||||
"roa_count": 910888,
|
||||
"atlas_asns_total": 4628,
|
||||
"atlas_asns_with_aspa": 421,
|
||||
"coverage_pct_atlas": 9.1,
|
||||
"aspa_delta": 4,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-29",
|
||||
"ts": 1780085968110,
|
||||
"aspa_objects": 2019,
|
||||
"roa_count": 916441,
|
||||
"atlas_asns_total": 4640,
|
||||
"atlas_asns_with_aspa": 424,
|
||||
"coverage_pct_atlas": 9.14,
|
||||
"aspa_delta": 2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-30",
|
||||
"ts": 1780174163595,
|
||||
"aspa_objects": 2026,
|
||||
"roa_count": 918362,
|
||||
"atlas_asns_total": 4655,
|
||||
"atlas_asns_with_aspa": 426,
|
||||
"coverage_pct_atlas": 9.15,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-05-31",
|
||||
"ts": 1780262359143,
|
||||
"aspa_objects": 2031,
|
||||
"roa_count": 918565,
|
||||
"atlas_asns_total": 4653,
|
||||
"atlas_asns_with_aspa": 427,
|
||||
"coverage_pct_atlas": 9.18,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-01",
|
||||
"ts": 1780350554630,
|
||||
"aspa_objects": 2040,
|
||||
"roa_count": 920847,
|
||||
"atlas_asns_total": 4673,
|
||||
"atlas_asns_with_aspa": 430,
|
||||
"coverage_pct_atlas": 9.2,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-02",
|
||||
"ts": 1780438750094,
|
||||
"aspa_objects": 2050,
|
||||
"roa_count": 925338,
|
||||
"atlas_asns_total": 4685,
|
||||
"atlas_asns_with_aspa": 435,
|
||||
"coverage_pct_atlas": 9.28,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-03",
|
||||
"ts": 1780526945736,
|
||||
"aspa_objects": 2054,
|
||||
"roa_count": 926656,
|
||||
"atlas_asns_total": 4692,
|
||||
"atlas_asns_with_aspa": 440,
|
||||
"coverage_pct_atlas": 9.38,
|
||||
"aspa_delta": 2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-04",
|
||||
"ts": 1780615141853,
|
||||
"aspa_objects": 2052,
|
||||
"roa_count": 929579,
|
||||
"atlas_asns_total": 4699,
|
||||
"atlas_asns_with_aspa": 438,
|
||||
"coverage_pct_atlas": 9.32,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-05",
|
||||
"ts": 1780703337103,
|
||||
"aspa_objects": 2057,
|
||||
"roa_count": 933266,
|
||||
"atlas_asns_total": 4533,
|
||||
"atlas_asns_with_aspa": 423,
|
||||
"coverage_pct_atlas": 9.33,
|
||||
"aspa_delta": 2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-06",
|
||||
"ts": 1780776833375,
|
||||
"aspa_objects": 2057,
|
||||
"roa_count": 933707,
|
||||
"atlas_asns_total": 4457,
|
||||
"atlas_asns_with_aspa": 415,
|
||||
"coverage_pct_atlas": 9.31,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-07",
|
||||
"ts": 1780865028684,
|
||||
"aspa_objects": 2060,
|
||||
"roa_count": 934451,
|
||||
"atlas_asns_total": 4624,
|
||||
"atlas_asns_with_aspa": 432,
|
||||
"coverage_pct_atlas": 9.34,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-08",
|
||||
"ts": 1780953225583,
|
||||
"aspa_objects": 2064,
|
||||
"roa_count": 935363,
|
||||
"atlas_asns_total": 4673,
|
||||
"atlas_asns_with_aspa": 439,
|
||||
"coverage_pct_atlas": 9.39,
|
||||
"aspa_delta": 2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-09",
|
||||
"ts": 1781043490445,
|
||||
"aspa_objects": 2082,
|
||||
"roa_count": 938346,
|
||||
"atlas_asns_total": 4693,
|
||||
"atlas_asns_with_aspa": 446,
|
||||
"coverage_pct_atlas": 9.5,
|
||||
"aspa_delta": 4,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-10",
|
||||
"ts": 1781121824287,
|
||||
"aspa_objects": 2086,
|
||||
"roa_count": 942559,
|
||||
"atlas_asns_total": 4692,
|
||||
"atlas_asns_with_aspa": 444,
|
||||
"coverage_pct_atlas": 9.46,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-11",
|
||||
"ts": 1781218962694,
|
||||
"aspa_objects": 2101,
|
||||
"roa_count": 949614,
|
||||
"atlas_asns_total": 4721,
|
||||
"atlas_asns_with_aspa": 449,
|
||||
"coverage_pct_atlas": 9.51,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-12",
|
||||
"ts": 1781305669463,
|
||||
"aspa_objects": 2104,
|
||||
"roa_count": 950161,
|
||||
"atlas_asns_total": 4716,
|
||||
"atlas_asns_with_aspa": 447,
|
||||
"coverage_pct_atlas": 9.48,
|
||||
"aspa_delta": 2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-13",
|
||||
"ts": 1781392376261,
|
||||
"aspa_objects": 2112,
|
||||
"roa_count": 950030,
|
||||
"atlas_asns_total": 4727,
|
||||
"atlas_asns_with_aspa": 450,
|
||||
"coverage_pct_atlas": 9.52,
|
||||
"aspa_delta": 2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-14",
|
||||
"ts": 1781479082907,
|
||||
"aspa_objects": 2115,
|
||||
"roa_count": 950083,
|
||||
"atlas_asns_total": 4731,
|
||||
"atlas_asns_with_aspa": 451,
|
||||
"coverage_pct_atlas": 9.53,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-15",
|
||||
"ts": 1781565789140,
|
||||
"aspa_objects": 2119,
|
||||
"roa_count": 950996,
|
||||
"atlas_asns_total": 4736,
|
||||
"atlas_asns_with_aspa": 450,
|
||||
"coverage_pct_atlas": 9.5,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-16",
|
||||
"ts": 1781649652082,
|
||||
"aspa_objects": 2130,
|
||||
"roa_count": 953250,
|
||||
"atlas_asns_total": 4733,
|
||||
"atlas_asns_with_aspa": 450,
|
||||
"coverage_pct_atlas": 9.51,
|
||||
"aspa_delta": 3,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-17",
|
||||
"ts": 1781734424437,
|
||||
"aspa_objects": 2133,
|
||||
"roa_count": 954510,
|
||||
"atlas_asns_total": 4726,
|
||||
"atlas_asns_with_aspa": 450,
|
||||
"coverage_pct_atlas": 9.52,
|
||||
"aspa_delta": -1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-18",
|
||||
"ts": 1781821528896,
|
||||
"aspa_objects": 2143,
|
||||
"roa_count": 957195,
|
||||
"atlas_asns_total": 4746,
|
||||
"atlas_asns_with_aspa": 450,
|
||||
"coverage_pct_atlas": 9.48,
|
||||
"aspa_delta": 3,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-19",
|
||||
"ts": 1781908099618,
|
||||
"aspa_objects": 2146,
|
||||
"roa_count": 957896,
|
||||
"atlas_asns_total": 4748,
|
||||
"atlas_asns_with_aspa": 452,
|
||||
"coverage_pct_atlas": 9.52,
|
||||
"aspa_delta": 2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-20",
|
||||
"ts": 1781994672218,
|
||||
"aspa_objects": 2151,
|
||||
"roa_count": 958031,
|
||||
"atlas_asns_total": 4735,
|
||||
"atlas_asns_with_aspa": 452,
|
||||
"coverage_pct_atlas": 9.55,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-21",
|
||||
"ts": 1782081244003,
|
||||
"aspa_objects": 2156,
|
||||
"roa_count": 958067,
|
||||
"atlas_asns_total": 4739,
|
||||
"atlas_asns_with_aspa": 453,
|
||||
"coverage_pct_atlas": 9.56,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-22",
|
||||
"ts": 1782167816179,
|
||||
"aspa_objects": 2163,
|
||||
"roa_count": 958365,
|
||||
"atlas_asns_total": 4746,
|
||||
"atlas_asns_with_aspa": 456,
|
||||
"coverage_pct_atlas": 9.61,
|
||||
"aspa_delta": 3,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-23",
|
||||
"ts": 1782253239274,
|
||||
"aspa_objects": 2173,
|
||||
"roa_count": 959022,
|
||||
"atlas_asns_total": 4743,
|
||||
"atlas_asns_with_aspa": 457,
|
||||
"coverage_pct_atlas": 9.64,
|
||||
"aspa_delta": -1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-24",
|
||||
"ts": 1782338059434,
|
||||
"aspa_objects": 2184,
|
||||
"roa_count": 959716,
|
||||
"atlas_asns_total": 4748,
|
||||
"atlas_asns_with_aspa": 459,
|
||||
"coverage_pct_atlas": 9.67,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-25",
|
||||
"ts": 1782422879426,
|
||||
"aspa_objects": 2194,
|
||||
"roa_count": 961420,
|
||||
"atlas_asns_total": 4749,
|
||||
"atlas_asns_with_aspa": 465,
|
||||
"coverage_pct_atlas": 9.79,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-26",
|
||||
"ts": 1782507700593,
|
||||
"aspa_objects": 2202,
|
||||
"roa_count": 961846,
|
||||
"atlas_asns_total": 4748,
|
||||
"atlas_asns_with_aspa": 462,
|
||||
"coverage_pct_atlas": 9.73,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-27",
|
||||
"ts": 1782592519478,
|
||||
"aspa_objects": 2208,
|
||||
"roa_count": 962630,
|
||||
"atlas_asns_total": 4746,
|
||||
"atlas_asns_with_aspa": 459,
|
||||
"coverage_pct_atlas": 9.67,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-28",
|
||||
"ts": 1782677339653,
|
||||
"aspa_objects": 2210,
|
||||
"roa_count": 962824,
|
||||
"atlas_asns_total": 4738,
|
||||
"atlas_asns_with_aspa": 462,
|
||||
"coverage_pct_atlas": 9.75,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-29",
|
||||
"ts": 1782776296119,
|
||||
"aspa_objects": 2218,
|
||||
"roa_count": 963499,
|
||||
"atlas_asns_total": 4749,
|
||||
"atlas_asns_with_aspa": 462,
|
||||
"coverage_pct_atlas": 9.73,
|
||||
"aspa_delta": 3,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-06-30",
|
||||
"ts": 1782861116475,
|
||||
"aspa_objects": 2227,
|
||||
"roa_count": 964524,
|
||||
"atlas_asns_total": 4747,
|
||||
"atlas_asns_with_aspa": 463,
|
||||
"coverage_pct_atlas": 9.75,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-01",
|
||||
"ts": 1782943528014,
|
||||
"aspa_objects": 2234,
|
||||
"roa_count": 965150,
|
||||
"atlas_asns_total": 4747,
|
||||
"atlas_asns_with_aspa": 470,
|
||||
"coverage_pct_atlas": 9.9,
|
||||
"aspa_delta": 3,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-02",
|
||||
"ts": 1783029743491,
|
||||
"aspa_objects": 2244,
|
||||
"roa_count": 965634,
|
||||
"atlas_asns_total": 4744,
|
||||
"atlas_asns_with_aspa": 473,
|
||||
"coverage_pct_atlas": 9.97,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-03",
|
||||
"ts": 1783110319303,
|
||||
"aspa_objects": 2253,
|
||||
"roa_count": 965995,
|
||||
"atlas_asns_total": 4739,
|
||||
"atlas_asns_with_aspa": 478,
|
||||
"coverage_pct_atlas": 10.09,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-04",
|
||||
"ts": 1783195400105,
|
||||
"aspa_objects": 2254,
|
||||
"roa_count": 966049,
|
||||
"atlas_asns_total": 4757,
|
||||
"atlas_asns_with_aspa": 480,
|
||||
"coverage_pct_atlas": 10.09,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-05",
|
||||
"ts": 1783282307341,
|
||||
"aspa_objects": 2264,
|
||||
"roa_count": 966292,
|
||||
"atlas_asns_total": 4740,
|
||||
"atlas_asns_with_aspa": 483,
|
||||
"coverage_pct_atlas": 10.19,
|
||||
"aspa_delta": 3,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-06",
|
||||
"ts": 1783368549946,
|
||||
"aspa_objects": 2275,
|
||||
"roa_count": 966360,
|
||||
"atlas_asns_total": 4736,
|
||||
"atlas_asns_with_aspa": 482,
|
||||
"coverage_pct_atlas": 10.18,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-07",
|
||||
"ts": 1783464285059,
|
||||
"aspa_objects": 2286,
|
||||
"roa_count": 967521,
|
||||
"atlas_asns_total": 4747,
|
||||
"atlas_asns_with_aspa": 483,
|
||||
"coverage_pct_atlas": 10.17,
|
||||
"aspa_delta": 3,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-08",
|
||||
"ts": 1783550943346,
|
||||
"aspa_objects": 2309,
|
||||
"roa_count": 967933,
|
||||
"atlas_asns_total": 4750,
|
||||
"atlas_asns_with_aspa": 486,
|
||||
"coverage_pct_atlas": 10.23,
|
||||
"aspa_delta": 1,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-09",
|
||||
"ts": 1783637598811,
|
||||
"aspa_objects": 2324,
|
||||
"roa_count": 969050,
|
||||
"atlas_asns_total": 4759,
|
||||
"atlas_asns_with_aspa": 492,
|
||||
"coverage_pct_atlas": 10.34,
|
||||
"aspa_delta": 2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-10",
|
||||
"ts": 1783724254815,
|
||||
"aspa_objects": 2347,
|
||||
"roa_count": 970180,
|
||||
"atlas_asns_total": 4766,
|
||||
"atlas_asns_with_aspa": 495,
|
||||
"coverage_pct_atlas": 10.39,
|
||||
"aspa_delta": 5,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-11",
|
||||
"ts": 1783810912350,
|
||||
"aspa_objects": 2352,
|
||||
"roa_count": 970494,
|
||||
"atlas_asns_total": 4765,
|
||||
"atlas_asns_with_aspa": 495,
|
||||
"coverage_pct_atlas": 10.39,
|
||||
"aspa_delta": 2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-12",
|
||||
"ts": 1783897568710,
|
||||
"aspa_objects": 2355,
|
||||
"roa_count": 970426,
|
||||
"atlas_asns_total": 4769,
|
||||
"atlas_asns_with_aspa": 500,
|
||||
"coverage_pct_atlas": 10.48,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-13",
|
||||
"ts": 1783984224592,
|
||||
"aspa_objects": 2361,
|
||||
"roa_count": 970721,
|
||||
"atlas_asns_total": 4780,
|
||||
"atlas_asns_with_aspa": 505,
|
||||
"coverage_pct_atlas": 10.56,
|
||||
"aspa_delta": 3,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-14",
|
||||
"ts": 1784071182188,
|
||||
"aspa_objects": 2373,
|
||||
"roa_count": 971705,
|
||||
"atlas_asns_total": 0,
|
||||
"atlas_asns_with_aspa": 0,
|
||||
"coverage_pct_atlas": 0,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-15",
|
||||
"ts": 1784159361526,
|
||||
"aspa_objects": 2382,
|
||||
"roa_count": 971732,
|
||||
"atlas_asns_total": 4775,
|
||||
"atlas_asns_with_aspa": 504,
|
||||
"coverage_pct_atlas": 10.55,
|
||||
"aspa_delta": 2,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-16",
|
||||
"ts": 1784174057994,
|
||||
"aspa_objects": 2382,
|
||||
"roa_count": 971831,
|
||||
"atlas_asns_total": 4775,
|
||||
"atlas_asns_with_aspa": 504,
|
||||
"coverage_pct_atlas": 10.55,
|
||||
"aspa_delta": 0,
|
||||
"method": "rpki_cloudflare_feed"
|
||||
}
|
||||
]
|
||||
1
audit/asn_pool.json
Normal file
1
audit/asn_pool.json
Normal file
File diff suppressed because one or more lines are too long
176
audit/daily_audit.py
Executable file
176
audit/daily_audit.py
Executable file
@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PeerCortex Final Quality Audit v4
|
||||
Tests 103 ASNs against v0.6.7 (running, stable)
|
||||
Uses cached responses where available, skips ASNs that don't respond in 15s
|
||||
Cross-validates: prefix counts, RPKI math, IX dedup, facilities, RIR, country
|
||||
"""
|
||||
import json, urllib.request, time, sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
def fetch(url, timeout=15):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "PeerCortex-Audit/4.0"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.loads(r.read())
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
ASNS = [
|
||||
1, 174, 714, 1299, 2497, 2516, 2906, 3257, 3303, 3320, 3356,
|
||||
4134, 4766, 5089, 5483, 5511, 6057, 6128, 6147, 6453, 6695,
|
||||
6762, 6830, 6939, 7018, 7473, 7545, 7922, 8075, 8151, 8220,
|
||||
8422, 8708, 9121, 9304, 9318, 9498, 10429, 10474, 10796, 12479,
|
||||
12714, 12956, 13237, 13335, 14080, 15133, 15169, 15600, 15704,
|
||||
16276, 16509, 17676, 18403, 19281, 20115, 20562, 20932, 20940,
|
||||
21928, 22773, 22822, 23106, 24115, 24429, 24940, 24961, 25091,
|
||||
29049, 32934, 33891, 34465, 34549, 35369, 37100, 38001, 39122,
|
||||
41327, 42, 42739, 43350, 43996, 44574, 45899, 47692, 48159,
|
||||
50629, 51088, 54113, 56630, 57695, 58057, 59947, 60068, 60501,
|
||||
62240, 64271, 132335, 136907, 137409, 139070, 197540, 199121,
|
||||
]
|
||||
|
||||
results = {"ok": 0, "warn": 0, "crit": 0, "skip": 0, "total": len(ASNS)}
|
||||
all_issues = []
|
||||
responded = []
|
||||
|
||||
print(f"PeerCortex Final Audit v4 — {datetime.now(timezone.utc).isoformat()}")
|
||||
print(f"Target: http://localhost:3101 | ASNs: {len(ASNS)}")
|
||||
print("=" * 75)
|
||||
|
||||
for i, asn in enumerate(ASNS):
|
||||
sys.stdout.write(f"\r [{i+1:3d}/{len(ASNS)}] AS{asn}... ")
|
||||
sys.stdout.flush()
|
||||
|
||||
d = fetch(f"http://localhost:3101/api/lookup?asn={asn}", timeout=15)
|
||||
if not d or "network" not in d or d.get("error"):
|
||||
results["skip"] += 1
|
||||
continue
|
||||
|
||||
responded.append(asn)
|
||||
n = d.get("network", {})
|
||||
p = d.get("prefixes", {})
|
||||
r = d.get("rpki", {})
|
||||
ix = d.get("ix_presence", {})
|
||||
fac = d.get("facilities", {})
|
||||
meta= d.get("meta", {})
|
||||
reg = d.get("registration", {})
|
||||
|
||||
issues = []
|
||||
|
||||
# ── Math checks ─────────────────────────────────────────
|
||||
if p.get("ipv4", 0) + p.get("ipv6", 0) != p.get("total", -1):
|
||||
issues.append(("CRITICAL", "prefix_math",
|
||||
f"v4({p.get('ipv4')})+v6({p.get('ipv6')})={p.get('ipv4',0)+p.get('ipv6',0)} ≠ total({p.get('total')})"))
|
||||
|
||||
rpki_sum = r.get("valid",0) + r.get("invalid",0) + r.get("not_found",0)
|
||||
if rpki_sum != r.get("checked", 0):
|
||||
issues.append(("CRITICAL", "rpki_sum",
|
||||
f"{rpki_sum} ≠ checked({r.get('checked')})"))
|
||||
|
||||
if r.get("checked", 0) > 0:
|
||||
exp = round(r.get("valid",0) / r.get("checked") * 100)
|
||||
if abs(exp - r.get("coverage_percent", 0)) > 1:
|
||||
issues.append(("CRITICAL", "rpki_pct",
|
||||
f"expected {exp}% got {r.get('coverage_percent')}%"))
|
||||
|
||||
conns = ix.get("connections", [])
|
||||
dedup = len(set(c.get("ix_id") for c in conns if c.get("ix_id")))
|
||||
if dedup != ix.get("unique_ixps", 0):
|
||||
issues.append(("CRITICAL", "ix_dedup",
|
||||
f"dedup={dedup} ≠ unique_ixps={ix.get('unique_ixps')}"))
|
||||
|
||||
if len(conns) != ix.get("total_connections", 0):
|
||||
issues.append(("WARNING", "ix_total",
|
||||
f"len={len(conns)} ≠ total_connections={ix.get('total_connections')}"))
|
||||
|
||||
if len(fac.get("list",[])) != fac.get("total", 0):
|
||||
issues.append(("WARNING", "fac_total",
|
||||
f"len={len(fac.get('list',[]))} ≠ total={fac.get('total')}"))
|
||||
|
||||
# ── Mandatory fields ─────────────────────────────────────
|
||||
if not n.get("name") or n.get("name") == "Unknown":
|
||||
issues.append(("CRITICAL", "name_empty", f"name={n.get('name')!r}"))
|
||||
|
||||
if not n.get("asn"):
|
||||
issues.append(("CRITICAL", "asn_empty", "ASN field missing"))
|
||||
|
||||
if not n.get("rir") and not reg.get("rir"):
|
||||
issues.append(("WARNING", "rir_empty", f"rir empty, country={n.get('country')!r}"))
|
||||
|
||||
if not n.get("country"):
|
||||
issues.append(("WARNING", "country_empty", "country field empty"))
|
||||
|
||||
if meta.get("version") != "0.6.7":
|
||||
issues.append(("WARNING", "version", f"got {meta.get('version')}"))
|
||||
|
||||
# ── Categorize ───────────────────────────────────────────
|
||||
crits = [x for x in issues if x[0] == "CRITICAL"]
|
||||
warns = [x for x in issues if x[0] == "WARNING"]
|
||||
|
||||
if crits:
|
||||
results["crit"] += 1
|
||||
elif warns:
|
||||
results["warn"] += 1
|
||||
else:
|
||||
results["ok"] += 1
|
||||
|
||||
if issues:
|
||||
all_issues.append((asn, n.get("name","?"), issues,
|
||||
p.get("total"), r.get("coverage_percent"),
|
||||
ix.get("total_connections"), fac.get("total"),
|
||||
n.get("rir"), n.get("country")))
|
||||
|
||||
print(f"\r Done. {len(responded)} responded, {results['skip']} skipped. ")
|
||||
|
||||
print()
|
||||
print("=" * 75)
|
||||
print(f"FINAL AUDIT SUMMARY — {len(responded)} ASNs responded ({results['skip']} timed out)")
|
||||
print("=" * 75)
|
||||
print(f" ✓ PERFECT (zero issues): {results['ok']:3d}")
|
||||
print(f" ⚠ WARNING only: {results['warn']:3d}")
|
||||
print(f" ✗ CRITICAL: {results['crit']:3d}")
|
||||
print(f" ⏭ Skipped (no response): {results['skip']:3d}")
|
||||
|
||||
if results["crit"] == 0:
|
||||
print("\n ✅ NO CRITICAL DATA ERRORS IN ANY RESPONDED ASN")
|
||||
|
||||
# Per-category counts
|
||||
warn_counts = {}
|
||||
crit_counts = {}
|
||||
for _, _, issues, *_ in all_issues:
|
||||
for sev, field, _ in issues:
|
||||
if sev == "CRITICAL":
|
||||
crit_counts[field] = crit_counts.get(field, 0) + 1
|
||||
else:
|
||||
warn_counts[field] = warn_counts.get(field, 0) + 1
|
||||
|
||||
if crit_counts:
|
||||
print("\n CRITICAL by field:")
|
||||
for f, c in sorted(crit_counts.items(), key=lambda x: -x[1]):
|
||||
print(f" {f:35s}: {c} ASNs")
|
||||
|
||||
if warn_counts:
|
||||
print("\n WARNING by field:")
|
||||
for f, c in sorted(warn_counts.items(), key=lambda x: -x[1]):
|
||||
print(f" {f:35s}: {c} ASNs")
|
||||
|
||||
# Detail for any issues
|
||||
if all_issues:
|
||||
print("\n DETAILS:")
|
||||
for asn, name, issues, pfx, rpki_pct, ix_cnt, fac_cnt, rir, cc in all_issues:
|
||||
for sev, field, msg in issues:
|
||||
print(f" [{sev[:4]}] AS{asn:>7} | {field:30s} | {msg[:60]}")
|
||||
|
||||
print()
|
||||
print(" SAMPLE DATA (responded ASNs):")
|
||||
print(f" {'ASN':>8} {'Name':24s} {'RIR':>6} {'CC':>3} {'Pfx':>5} {'RPKI%':>6} {'IX':>4} {'Fac':>4}")
|
||||
print(f" {'-'*70}")
|
||||
for asn in responded[:25]:
|
||||
d = fetch(f"http://localhost:3101/api/lookup?asn={asn}", timeout=5)
|
||||
if not d: continue
|
||||
n=d.get("network",{}); p=d.get("prefixes",{}); r=d.get("rpki",{}); ix=d.get("ix_presence",{}); fac=d.get("facilities",{})
|
||||
print(f" AS{asn:>6} {n.get('name','?')[:22]:24s} {n.get('rir','?'):>6} {n.get('country','?'):>3} {p.get('total',0):>5} {r.get('coverage_percent',0):>5}% {ix.get('total_connections',0):>4} {fac.get('total',0):>4}")
|
||||
|
||||
print()
|
||||
print(f"Audit complete: {datetime.now(timezone.utc).isoformat()}")
|
||||
270
audit/rotating_audit.py
Normal file
270
audit/rotating_audit.py
Normal file
@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PeerCortex Rotating Daily Audit
|
||||
- Pool of 500+ ASNs
|
||||
- Each day: deterministic 100-ASN selection (seeded by date → reproducible, never same day twice)
|
||||
- Each ASN: PeerCortex response cross-validated against RIPE Stat + PeeringDB externally
|
||||
- Math checks: prefix sums, RPKI sums, IX dedup, facility counts
|
||||
- Results saved to /var/log/peercortex/audit-YYYY-MM-DD.json
|
||||
"""
|
||||
import json, urllib.request, urllib.error, time, sys, os
|
||||
from datetime import datetime, timezone
|
||||
from hashlib import sha256
|
||||
|
||||
# ── ASN Pool (500+ diverse networks across all RIRs) ──────────────────────────
|
||||
ASN_POOL = [
|
||||
# Tier-1 / Large Transit
|
||||
1, 174, 209, 286, 701, 702, 1239, 1273, 1280, 1299, 2914, 3257, 3320,
|
||||
3356, 3491, 3549, 3561, 4134, 4637, 4657, 5511, 6453, 6461, 6762, 6830,
|
||||
7018, 7473, 7922, 8220, 8928, 9002, 9304, 12956, 13030,
|
||||
# Cloud / Hyperscaler
|
||||
714, 2906, 8075, 13335, 15169, 16509, 16591, 20940, 21342, 32934, 36351,
|
||||
54113, 136907, 14061, 19604, 63949, 22616,
|
||||
# European ISPs
|
||||
1257, 1853, 2119, 2603, 3209, 3215, 3216, 3233, 3243, 3269, 3292, 3301,
|
||||
3303, 3308, 3327, 5089, 5390, 5432, 5483, 6057, 6128, 6147, 6695, 6728,
|
||||
6830, 6939, 8220, 8359, 8400, 8422, 8447, 8468, 8473, 8551, 8708, 9121,
|
||||
9145, 9158, 12714, 13237, 15435, 15547, 15600, 15692, 15830, 16276,
|
||||
20562, 20932, 21202, 24940, 24961, 25091, 28716, 29049, 29208, 33891,
|
||||
34465, 34549, 35369, 39122, 41327, 42, 42739, 43350, 47692, 48159,
|
||||
50629, 51088, 58057, 197540, 199121,
|
||||
# APNIC
|
||||
2497, 2516, 4766, 7545, 9318, 9498, 17676, 18403, 23969, 24115, 38001,
|
||||
45899, 55836, 132335, 136907, 137409, 139070, 4713, 9269, 17488, 23944,
|
||||
38197, 45352, 63927, 131090, 133481, 134762, 137718, 149440,
|
||||
# ARIN
|
||||
701, 1239, 1280, 2828, 3549, 3561, 6128, 6939, 7018, 7922, 10429, 10796,
|
||||
11427, 13977, 14080, 15133, 19281, 20115, 20325, 21928, 22773, 22822,
|
||||
23106, 32934, 33070, 35911, 36351, 40191, 43996, 54113, 62240, 64271,
|
||||
393398, 11404, 14618, 14593, 7046, 26101, 10310, 32590,
|
||||
# LACNIC
|
||||
6147, 8167, 10429, 10474, 11419, 11644, 14080, 18881, 22927, 23106,
|
||||
28573, 52320, 53013, 61832, 262589, 263702,
|
||||
# AFRINIC
|
||||
10474, 12091, 23889, 24835, 30844, 36864, 36903, 37100, 37271, 37468,
|
||||
55330, 327814,
|
||||
# IXPs / Route Servers
|
||||
6695, 24115, 34307, 42476, 43366, 47498, 56393,
|
||||
# CDN / Hosting
|
||||
2906, 8100, 13335, 15133, 16509, 20940, 22822, 24429, 46489, 54113,
|
||||
60068, 60501, 62240, 64271, 132335,
|
||||
# Security / DNS
|
||||
19281, 15169, 13335, 714, 20473,
|
||||
# Mixed / Small
|
||||
29791, 34224, 35332, 39386, 41695, 42473, 44574, 47065, 48858, 50300,
|
||||
56630, 57695, 58057, 59947, 60068, 60501, 63023, 64271, 132335,
|
||||
# More European
|
||||
1200, 2611, 3265, 5388, 5430, 5444, 6128, 8309, 8365, 8560, 8607,
|
||||
9009, 12897, 13285, 15516, 16150, 20738, 21263, 24586, 24875, 25369,
|
||||
28917, 29017, 30036, 30781, 31025, 31400, 33986, 34006, 34224, 34655,
|
||||
35432, 39386, 39737, 41327, 41552, 42652, 43531, 44574, 47065,
|
||||
# Telcos worldwide
|
||||
2516, 3320, 3786, 4766, 4800, 5483, 7473, 8151, 8708, 9121, 9304,
|
||||
9318, 9498, 10429, 12479, 12714, 15704, 16276, 17676, 18403, 22773,
|
||||
]
|
||||
# Deduplicate while preserving order
|
||||
seen = set()
|
||||
ASN_POOL_DEDUP = [x for x in ASN_POOL if not (x in seen or seen.add(x))]
|
||||
|
||||
def load_pool():
|
||||
"""Load ASN pool from file, fall back to built-in list."""
|
||||
pool_file = os.path.join(os.path.dirname(__file__), 'asn_pool.json')
|
||||
try:
|
||||
with open(pool_file) as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return ASN_POOL_DEDUP
|
||||
|
||||
def select_daily_asns(date_str, count=100):
|
||||
"""Deterministic daily selection — same date always gives same 100 ASNs."""
|
||||
import random
|
||||
pool = load_pool()
|
||||
seed = int(sha256(date_str.encode()).hexdigest(), 16)
|
||||
rng = random.Random(seed)
|
||||
rng.shuffle(pool)
|
||||
return sorted(pool[:count])
|
||||
|
||||
def fetch(url, timeout=12):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "PeerCortex-Audit/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.loads(r.read())
|
||||
except:
|
||||
return None
|
||||
|
||||
def fetch_pc(asn, timeout=20):
|
||||
try:
|
||||
with urllib.request.urlopen(f"http://localhost:3101/api/lookup?asn={asn}", timeout=timeout) as r:
|
||||
return json.loads(r.read())
|
||||
except:
|
||||
return None
|
||||
|
||||
def audit_asn(asn):
|
||||
issues = []
|
||||
result = {"asn": asn, "status": "ok", "issues": [], "data": {}}
|
||||
|
||||
# ── 1. PeerCortex response ───────────────────────────────────────────────
|
||||
d = fetch_pc(asn)
|
||||
if not d or "network" not in d or d.get("error"):
|
||||
result["status"] = "skip"
|
||||
result["issues"].append(("SKIP", "no_response", "PeerCortex did not respond"))
|
||||
return result
|
||||
|
||||
n = d.get("network", {})
|
||||
p = d.get("prefixes", {})
|
||||
r = d.get("rpki", {})
|
||||
ix = d.get("ix_presence", {})
|
||||
fac = d.get("facilities", {})
|
||||
meta= d.get("meta", {})
|
||||
|
||||
result["data"] = {
|
||||
"name": n.get("name"),
|
||||
"rir": n.get("rir"),
|
||||
"cc": n.get("country"),
|
||||
"pfx_total": p.get("total", 0),
|
||||
"rpki_pct": r.get("coverage_percent", 0),
|
||||
"ix_cnt": ix.get("total_connections", 0),
|
||||
"fac_cnt": fac.get("total", 0),
|
||||
"version": meta.get("version"),
|
||||
}
|
||||
|
||||
# ── 2. Math checks (internal consistency) ───────────────────────────────
|
||||
if p.get("ipv4", 0) + p.get("ipv6", 0) != p.get("total", -1):
|
||||
issues.append(("CRITICAL", "prefix_math",
|
||||
f"v4({p.get('ipv4')})+v6({p.get('ipv6')}) ≠ total({p.get('total')})"))
|
||||
|
||||
rpki_sum = r.get("valid", 0) + r.get("invalid", 0) + r.get("not_found", 0)
|
||||
if rpki_sum != r.get("checked", 0):
|
||||
issues.append(("CRITICAL", "rpki_sum",
|
||||
f"{rpki_sum} ≠ checked({r.get('checked')})"))
|
||||
|
||||
if r.get("checked", 0) > 0:
|
||||
exp = round(r.get("valid", 0) / r.get("checked") * 100)
|
||||
if abs(exp - r.get("coverage_percent", 0)) > 1:
|
||||
issues.append(("CRITICAL", "rpki_pct",
|
||||
f"expected {exp}% got {r.get('coverage_percent')}%"))
|
||||
|
||||
conns = ix.get("connections", [])
|
||||
dedup = len(set(c.get("ix_id") for c in conns if c.get("ix_id")))
|
||||
if dedup != ix.get("unique_ixps", 0):
|
||||
issues.append(("CRITICAL", "ix_dedup",
|
||||
f"dedup={dedup} ≠ unique_ixps={ix.get('unique_ixps')}"))
|
||||
|
||||
if len(conns) != ix.get("total_connections", 0):
|
||||
issues.append(("WARNING", "ix_total",
|
||||
f"len={len(conns)} ≠ total_connections={ix.get('total_connections')}"))
|
||||
|
||||
if len(fac.get("list", [])) != fac.get("total", 0):
|
||||
issues.append(("WARNING", "fac_total",
|
||||
f"len={len(fac.get('list',[]))} ≠ total={fac.get('total')}"))
|
||||
|
||||
# ── 3. Mandatory fields ──────────────────────────────────────────────────
|
||||
if not n.get("name") or n.get("name") == "Unknown":
|
||||
issues.append(("CRITICAL", "name_empty", f"name={n.get('name')!r}"))
|
||||
|
||||
if not n.get("asn"):
|
||||
issues.append(("CRITICAL", "asn_field", "ASN field missing in response"))
|
||||
|
||||
if not n.get("rir"):
|
||||
issues.append(("WARNING", "rir_empty", f"rir empty, country={n.get('country')!r}"))
|
||||
|
||||
if not n.get("country"):
|
||||
issues.append(("WARNING", "country_empty", "country field empty"))
|
||||
|
||||
# ── 4. External cross-validation: RIPE Stat prefix count ────────────────
|
||||
ripe = fetch(f"https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS{asn}")
|
||||
if ripe and ripe.get("data", {}).get("prefixes") is not None:
|
||||
ripe_pfx = len(ripe["data"]["prefixes"])
|
||||
pc_pfx = p.get("total", 0)
|
||||
result["data"]["ripe_stat_pfx"] = ripe_pfx
|
||||
if ripe_pfx > 0 and pc_pfx > 0:
|
||||
ratio = min(ripe_pfx, pc_pfx) / max(ripe_pfx, pc_pfx)
|
||||
if ratio < 0.85:
|
||||
issues.append(("WARNING", "prefix_ext_mismatch",
|
||||
f"PC={pc_pfx} vs RIPE Stat={ripe_pfx} ({round((1-ratio)*100)}% diff)"))
|
||||
elif ripe_pfx == 0 and pc_pfx > 0:
|
||||
issues.append(("WARNING", "prefix_ext_zero",
|
||||
f"RIPE Stat reports 0 prefixes, PC shows {pc_pfx}"))
|
||||
|
||||
# ── 5. External cross-validation: PeeringDB IX count ────────────────────
|
||||
pdb = fetch(f"https://www.peeringdb.com/api/netixlan?asn={asn}&depth=0")
|
||||
if pdb and isinstance(pdb.get("data"), list):
|
||||
pdb_ix = len(pdb["data"])
|
||||
pc_ix = ix.get("total_connections", 0)
|
||||
result["data"]["pdb_ix_cnt"] = pdb_ix
|
||||
if pdb_ix > 0 and pc_ix > 0:
|
||||
ratio = min(pdb_ix, pc_ix) / max(pdb_ix, pc_ix)
|
||||
if ratio < 0.85:
|
||||
issues.append(("WARNING", "ix_ext_mismatch",
|
||||
f"PC={pc_ix} vs PeeringDB={pdb_ix} ({round((1-ratio)*100)}% diff)"))
|
||||
|
||||
# ── Categorize ───────────────────────────────────────────────────────────
|
||||
result["issues"] = issues
|
||||
crits = [x for x in issues if x[0] == "CRITICAL"]
|
||||
warns = [x for x in issues if x[0] == "WARNING"]
|
||||
if crits:
|
||||
result["status"] = "critical"
|
||||
elif warns:
|
||||
result["status"] = "warning"
|
||||
else:
|
||||
result["status"] = "ok"
|
||||
|
||||
return result
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
if __name__ == "__main__":
|
||||
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
asns = select_daily_asns(date_str, 100)
|
||||
start = time.time()
|
||||
|
||||
print(f"PeerCortex Rotating Audit — {date_str}")
|
||||
print(f"Pool: {len(ASN_POOL_DEDUP)} ASNs | Today: {len(asns)} selected")
|
||||
print("=" * 70)
|
||||
|
||||
results = []
|
||||
counts = {"ok": 0, "warning": 0, "critical": 0, "skip": 0}
|
||||
|
||||
for i, asn in enumerate(asns):
|
||||
sys.stdout.write(f"\r [{i+1:3d}/100] AS{asn}... ")
|
||||
sys.stdout.flush()
|
||||
r = audit_asn(asn)
|
||||
results.append(r)
|
||||
counts[r["status"]] += 1
|
||||
time.sleep(0.3) # be gentle to external APIs
|
||||
|
||||
elapsed = time.time() - start
|
||||
print(f"\r Done in {elapsed:.0f}s" + " " * 30)
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────
|
||||
print()
|
||||
print("=" * 70)
|
||||
print(f"SUMMARY — {date_str}")
|
||||
print("=" * 70)
|
||||
print(f" ✓ PERFECT: {counts['ok']:3d}")
|
||||
print(f" ⚠ WARNING: {counts['warning']:3d}")
|
||||
print(f" ✗ CRITICAL: {counts['critical']:3d}")
|
||||
print(f" ⏭ SKIP: {counts['skip']:3d}")
|
||||
|
||||
issues_found = [r for r in results if r["status"] in ("critical", "warning")]
|
||||
if issues_found:
|
||||
print(f"\n ISSUES ({len(issues_found)} ASNs):")
|
||||
for r in issues_found:
|
||||
for sev, field, msg in r["issues"]:
|
||||
print(f" [{sev[:4]}] AS{r['asn']:>7} | {field:30s} | {msg[:55]}")
|
||||
|
||||
# ── Save JSON report ──────────────────────────────────────────────────────
|
||||
report = {
|
||||
"date": date_str,
|
||||
"elapsed_s": round(elapsed),
|
||||
"asns_tested": asns,
|
||||
"pool_size": len(ASN_POOL_DEDUP),
|
||||
"counts": counts,
|
||||
"results": results,
|
||||
"generated": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
out_path = f"/var/log/peercortex/audit-{date_str}.json"
|
||||
os.makedirs("/var/log/peercortex", exist_ok=True)
|
||||
with open(out_path, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
print(f"\nReport: {out_path}")
|
||||
print(f"Audit complete: {datetime.now(timezone.utc).isoformat()}")
|
||||
6
audit/run_daily_audit.sh
Executable file
6
audit/run_daily_audit.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
LOG=/var/log/peercortex/audit-${DATE}.log
|
||||
find /var/log/peercortex/ -name 'audit-*.log' -mtime +30 -delete 2>/dev/null
|
||||
find /var/log/peercortex/ -name 'audit-*.json' -mtime +30 -delete 2>/dev/null
|
||||
python3 /opt/peercortex-app/audit/rotating_audit.py > "$LOG" 2>&1
|
||||
226
audit/send_audit_report.py
Executable file
226
audit/send_audit_report.py
Executable file
@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PeerCortex Daily Audit Mailer — detailed report via localhost:25"""
|
||||
import os, json, smtplib
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import make_msgid, formatdate
|
||||
|
||||
SMTP_HOST = "localhost"
|
||||
SMTP_PORT = 25
|
||||
MAIL_FROM = "ctx-report@fichtmueller.org"
|
||||
MAIL_TO = "rf@flexoptix.net"
|
||||
|
||||
def td(content, color=None, align="left", mono=False, bold=False):
|
||||
ff = "monospace" if mono else "sans-serif"
|
||||
fw = "font-weight:700;" if bold else ""
|
||||
cl = "color:%s;" % color if color else ""
|
||||
return '<td style="font-family:%s;font-size:11px;padding:3px 8px;text-align:%s;%s%s">%s</td>' % (ff, align, cl, fw, content)
|
||||
|
||||
def load_report():
|
||||
for delta in [0, 1]:
|
||||
d = (datetime.now(timezone.utc) - timedelta(days=delta)).strftime("%Y-%m-%d")
|
||||
try:
|
||||
with open("/var/log/peercortex/audit-%s.json" % d) as f:
|
||||
return json.load(f), d
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
return None, datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
|
||||
def build(report, date_str):
|
||||
if not report:
|
||||
return ("<html><body style='background:#0f172a;color:#e2e8f0;padding:24px'><h2>No report for %s</h2></body></html>" % date_str,
|
||||
"PeerCortex Audit %s — no report" % date_str)
|
||||
|
||||
counts = report.get("counts", {})
|
||||
results = report.get("results", [])
|
||||
elapsed = report.get("elapsed_s", 0)
|
||||
pool = report.get("pool_size", 0)
|
||||
ok = counts.get("ok", 0)
|
||||
warn = counts.get("warning", 0)
|
||||
crit = counts.get("critical", 0)
|
||||
skip = counts.get("skip", 0)
|
||||
total = len(results)
|
||||
mins, secs = elapsed // 60, elapsed % 60
|
||||
|
||||
if crit == 0 and warn == 0:
|
||||
sc, emoji, text, spfx = "#22c55e", "✅", "All Clear — no issues detected", "✅ All Clear"
|
||||
elif crit == 0:
|
||||
sc, emoji, text, spfx = "#f97316", "⚠️", "%d warning(s)" % warn, "⚠️ %d Warning(s)" % warn
|
||||
else:
|
||||
sc, emoji, text, spfx = "#ef4444", "🚨", "%d CRITICAL issue(s) — action required" % crit, "🚨 %d CRITICAL" % crit
|
||||
|
||||
subject = "PeerCortex Daily Audit %s — %s" % (date_str, spfx)
|
||||
|
||||
# Issues
|
||||
issue_results = [r for r in results if r.get("status") in ("critical", "warning")]
|
||||
issues_html = ""
|
||||
if issue_results:
|
||||
rows = ""
|
||||
for r in issue_results:
|
||||
asn = str(r["asn"])
|
||||
name = (r.get("data", {}).get("name") or "?")[:28]
|
||||
for sev, field, msg in r.get("issues", []):
|
||||
c = "#ef4444" if sev == "CRITICAL" else "#f97316"
|
||||
rows += "<tr style='border-bottom:1px solid #1e293b'>%s%s%s%s%s</tr>" % (
|
||||
td("[%s]" % sev[:4], c, mono=True),
|
||||
td("AS" + asn, "#94a3b8", mono=True),
|
||||
td(name, "#e2e8f0"),
|
||||
td(field, "#64748b", mono=True),
|
||||
td(msg[:60], "#94a3b8"))
|
||||
issues_html = (
|
||||
"<h3 style='color:#f97316;font-size:12px;margin:24px 0 6px;text-transform:uppercase'>⚠ Issues (%d ASNs)</h3>" % len(issue_results) +
|
||||
"<table style='width:100%%;border-collapse:collapse;background:#111827;border-radius:6px;overflow:hidden'>%s</table>" % rows
|
||||
)
|
||||
|
||||
# Skipped
|
||||
skip_results = [r for r in results if r.get("status") == "skip"]
|
||||
skipped_html = ""
|
||||
if skip_results:
|
||||
rows = ""
|
||||
for r in skip_results:
|
||||
asn = str(r["asn"])
|
||||
reason = r.get("issues", [["SKIP","","no response from PeerCortex"]])[0][2] if r.get("issues") else "no response from PeerCortex"
|
||||
url = "https://peercortex.org/?asn=" + asn
|
||||
rows += ("<tr style='border-bottom:1px solid #1e293b'>%s%s<td style='font-size:11px;padding:3px 8px'>"
|
||||
"<a href='%s' style='color:#3b82f6;text-decoration:none'>→ lookup</a></td></tr>") % (
|
||||
td("AS" + asn, "#94a3b8", mono=True),
|
||||
td(reason, "#64748b"),
|
||||
url)
|
||||
skipped_html = (
|
||||
"<h3 style='color:#475569;font-size:12px;margin:24px 0 6px;text-transform:uppercase'>⏭ Skipped ASNs (%d) — no PeerCortex response</h3>" % skip +
|
||||
"<p style='color:#475569;font-size:11px;margin:0 0 8px'>Possible causes: ASN not in any routing table, very large prefix count, or temporary API timeout.</p>" +
|
||||
"<table style='width:100%%;border-collapse:collapse;background:#111827;border-radius:6px;overflow:hidden'>%s</table>" % rows
|
||||
)
|
||||
|
||||
# Low RPKI
|
||||
rpki_bad = sorted(
|
||||
[r for r in results if r.get("status") != "skip" and r.get("data", {}).get("rpki_pct", 100) < 60],
|
||||
key=lambda r: r.get("data", {}).get("rpki_pct", 100)
|
||||
)[:8]
|
||||
rpki_html = ""
|
||||
if rpki_bad:
|
||||
rows = ""
|
||||
for r in rpki_bad:
|
||||
d = r.get("data", {})
|
||||
pct = d.get("rpki_pct", 0)
|
||||
bar = ("<div style='background:#1e293b;border-radius:3px;height:8px;width:100px;display:inline-block'>"
|
||||
"<div style='background:#ef4444;border-radius:3px;height:8px;width:%d%%'></div></div>") % max(2, pct)
|
||||
rows += "<tr style='border-bottom:1px solid #1e293b'>%s%s%s<td style='padding:3px 8px'>%s</td>%s</tr>" % (
|
||||
td("AS" + str(r["asn"]), "#94a3b8", mono=True),
|
||||
td((d.get("name") or "?")[:28], "#e2e8f0"),
|
||||
td(d.get("rir", "?"), "#64748b", "center"),
|
||||
bar,
|
||||
td(str(pct) + "%", "#ef4444", "right", bold=True))
|
||||
rpki_html = (
|
||||
"<h3 style='color:#94a3b8;font-size:12px;margin:24px 0 6px;text-transform:uppercase'>📉 Low RPKI Coverage (<60%%)</h3>" +
|
||||
"<table style='width:100%%;border-collapse:collapse;background:#111827;border-radius:6px;overflow:hidden'>%s</table>" % rows
|
||||
)
|
||||
|
||||
# All ASNs table
|
||||
sorted_results = sorted([r for r in results if r.get("status") != "skip"], key=lambda x: x["asn"])
|
||||
header = ("<tr style='background:#1e293b'>%s%s%s%s%s%s%s%s%s%s%s</tr>" % (
|
||||
td("ASN", "#64748b", "right", mono=True),
|
||||
td("Name", "#64748b"),
|
||||
td("RIR", "#64748b", "center"),
|
||||
td("CC", "#64748b", "center"),
|
||||
td("Pfx", "#64748b", "right"),
|
||||
td("RIPE Stat", "#64748b", "right"),
|
||||
td("RPKI%", "#64748b", "right"),
|
||||
td("IX", "#64748b", "right"),
|
||||
td("PDB IX", "#64748b", "right"),
|
||||
td("Fac", "#64748b", "right"),
|
||||
td("Status", "#64748b", "center")))
|
||||
rows = ""
|
||||
for i, r in enumerate(sorted_results):
|
||||
d = r.get("data", {})
|
||||
bg = "#0f172a" if i % 2 == 0 else "#111827"
|
||||
st = r.get("status", "ok")
|
||||
if st == "critical":
|
||||
st_html = '<span style="color:#ef4444;font-weight:700">✗ CRIT</span>'
|
||||
elif st == "warning":
|
||||
st_html = '<span style="color:#f97316">⚠ WARN</span>'
|
||||
else:
|
||||
st_html = '<span style="color:#22c55e">✓</span>'
|
||||
pct = d.get("rpki_pct", 100)
|
||||
rpki_c = "#ef4444" if pct < 50 else "#f97316" if pct < 80 else "#94a3b8"
|
||||
ripe = str(d.get("ripe_stat_pfx", "—"))
|
||||
pdb = str(d.get("pdb_ix_cnt", "—"))
|
||||
rows += "<tr style='background:%s'>%s%s%s%s%s%s%s%s%s%s<td style='font-size:11px;padding:3px 8px;text-align:center'>%s</td></tr>" % (
|
||||
bg,
|
||||
td("AS" + str(r["asn"]), "#64748b", "right", mono=True),
|
||||
td((d.get("name") or "?")[:28], "#e2e8f0"),
|
||||
td(d.get("rir", "?"), "#94a3b8", "center"),
|
||||
td(d.get("cc", "?"), "#94a3b8", "center"),
|
||||
td(str(d.get("pfx_total", 0)), "#94a3b8", "right", mono=True),
|
||||
td(ripe, "#64748b", "right", mono=True),
|
||||
td(str(pct) + "%", rpki_c, "right", mono=True),
|
||||
td(str(d.get("ix_cnt", 0)), "#94a3b8", "right", mono=True),
|
||||
td(pdb, "#64748b", "right", mono=True),
|
||||
td(str(d.get("fac_cnt", 0)), "#94a3b8", "right", mono=True),
|
||||
st_html)
|
||||
|
||||
all_table = (
|
||||
"<h3 style='color:#94a3b8;font-size:12px;margin:24px 0 6px;text-transform:uppercase'>All Tested ASNs (%d)</h3>" % len(sorted_results) +
|
||||
"<div style='overflow-x:auto'><table style='width:100%%;border-collapse:collapse;border-radius:6px;overflow:hidden'>%s%s</table></div>" % (header, rows)
|
||||
)
|
||||
|
||||
html = """<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"></head>
|
||||
<body style="margin:0;padding:0;background:#0f172a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
|
||||
<div style="max-width:900px;margin:0 auto;padding:24px 16px">
|
||||
<div style="margin-bottom:20px">
|
||||
<span style="color:#3b82f6;font-weight:700;font-size:18px;letter-spacing:1px">PEER</span><span style="color:#fff;font-weight:700;font-size:18px;letter-spacing:1px">CORTEX</span>
|
||||
<span style="color:#475569;font-size:12px;margin-left:10px">Daily Audit Report · %(date)s</span>
|
||||
</div>
|
||||
<div style="background:%(sc)s18;border:1px solid %(sc)s40;border-radius:8px;padding:14px 18px;margin-bottom:20px">
|
||||
<div style="font-size:17px;font-weight:700;color:%(sc)s">%(emoji)s %(text)s</div>
|
||||
<div style="color:#94a3b8;font-size:12px;margin-top:4px">%(total)d ASNs tested from pool of %(pool)d · Runtime: %(mins)dm %(secs)ds · Cross-validated: RIPE Stat + PeeringDB</div>
|
||||
</div>
|
||||
<table style="width:100%%;border-collapse:collapse;margin-bottom:24px"><tr>
|
||||
<td style="width:22%%;text-align:center;background:#1e293b;border-radius:8px;padding:16px 8px">
|
||||
<div style="font-size:32px;font-weight:700;color:#22c55e">%(ok)d</div><div style="color:#64748b;font-size:10px;margin-top:4px">PERFECT</div></td>
|
||||
<td style="width:3%%"></td>
|
||||
<td style="width:22%%;text-align:center;background:#1e293b;border-radius:8px;padding:16px 8px">
|
||||
<div style="font-size:32px;font-weight:700;color:#f97316">%(warn)d</div><div style="color:#64748b;font-size:10px;margin-top:4px">WARNINGS</div></td>
|
||||
<td style="width:3%%"></td>
|
||||
<td style="width:22%%;text-align:center;background:#1e293b;border-radius:8px;padding:16px 8px">
|
||||
<div style="font-size:32px;font-weight:700;color:#ef4444">%(crit)d</div><div style="color:#64748b;font-size:10px;margin-top:4px">CRITICAL</div></td>
|
||||
<td style="width:3%%"></td>
|
||||
<td style="width:22%%;text-align:center;background:#1e293b;border-radius:8px;padding:16px 8px">
|
||||
<div style="font-size:32px;font-weight:700;color:#475569">%(skip)d</div><div style="color:#64748b;font-size:10px;margin-top:4px">SKIPPED</div></td>
|
||||
</tr></table>
|
||||
%(issues)s %(skipped)s %(rpki)s %(alltable)s
|
||||
<div style="margin-top:32px;padding-top:14px;border-top:1px solid #1e293b;color:#475569;font-size:11px">
|
||||
PeerCortex · <a href="https://peercortex.org" style="color:#3b82f6;text-decoration:none">peercortex.org</a>
|
||||
· 100 ASNs rotate daily from %(pool)d ASN pool · Audit at 02:00 UTC, report at 06:00 UTC
|
||||
</div>
|
||||
</div></body></html>""" % dict(
|
||||
date=date_str, sc=sc, emoji=emoji, text=text,
|
||||
total=total, pool=pool, mins=mins, secs=secs,
|
||||
ok=ok, warn=warn, crit=crit, skip=skip,
|
||||
issues=issues_html, skipped=skipped_html,
|
||||
rpki=rpki_html, alltable=all_table)
|
||||
|
||||
return html, subject
|
||||
|
||||
|
||||
def send_email(subject, html_body):
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = MAIL_FROM
|
||||
msg["To"] = MAIL_TO
|
||||
msg["Date"] = formatdate(localtime=True)
|
||||
msg["Message-ID"] = make_msgid(domain="fichtmueller.org")
|
||||
msg.attach(MIMEText("PeerCortex Daily Audit — please view in an HTML-capable email client.\n\nhttps://peercortex.org", "plain", "utf-8"))
|
||||
msg.attach(MIMEText(html_body, "html", "utf-8"))
|
||||
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=15) as s:
|
||||
s.sendmail(MAIL_FROM, [MAIL_TO], msg.as_bytes())
|
||||
print("Sent '%s' -> %s" % (subject, MAIL_TO))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
report, date_str = load_report()
|
||||
html, subject = build(report, date_str)
|
||||
print("Email size: %d bytes" % len(html.encode()))
|
||||
send_email(subject, html)
|
||||
3
audit/send_audit_report.sh
Executable file
3
audit/send_audit_report.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
export $(grep -E '^SMTP' /opt/peercortex-app/.env | xargs)
|
||||
python3 /opt/peercortex-app/audit/send_audit_report.py
|
||||
9
deploy-from-scp.sh
Executable file
9
deploy-from-scp.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
# Deploy PeerCortex from pre-copied files (server.js already in place via SCP)
|
||||
BACKUP_DIR=/opt/peercortex-app/backups
|
||||
mkdir -p $BACKUP_DIR
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
cp /opt/peercortex-app/server.js $BACKUP_DIR/server.js.$TS
|
||||
ls -t $BACKUP_DIR/server.js.* 2>/dev/null | tail -n +21 | xargs rm -f 2>/dev/null
|
||||
echo "[backup] Saved server.js ($TS)"
|
||||
pm2 restart peercortex
|
||||
24550
hijack-alerts.json
Normal file
24550
hijack-alerts.json
Normal file
File diff suppressed because it is too large
Load Diff
804
package-lock.json
generated
804
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -70,6 +70,7 @@
|
||||
"cheerio": "^1.0.0",
|
||||
"node-whois": "^2.1.3",
|
||||
"ollama": "^0.5.12",
|
||||
"puppeteer": "^24.42.0",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -415,6 +415,18 @@ body.dark .card{border-top-color:#e8e4dc}
|
||||
.ac-name { font-family: var(--body); font-size: .82rem; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ac-meta { font-family: var(--mono); font-size: .6rem; color: var(--muted); white-space: nowrap; }
|
||||
.ac-source { font-family: var(--mono); font-size: .55rem; color: var(--dim); margin-left: auto; padding-left: .4rem; white-space: nowrap; }
|
||||
|
||||
.pdf-menu-item {
|
||||
display: block;
|
||||
padding: .45rem 1rem;
|
||||
font-family: var(--mono);
|
||||
font-size: .72rem;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.pdf-menu-item:last-child { border-bottom: none; }
|
||||
.pdf-menu-item:hover { background: var(--purple); color: #fff !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -437,6 +449,7 @@ body.dark .card{border-top-color:#e8e4dc}
|
||||
<a href="https://atlas.ripe.net" target="_blank">RIPE Atlas</a>
|
||||
<a href="https://www.routeviews.org" target="_blank">Route Views</a>
|
||||
<a href="https://bgproutes.io" target="_blank">bgproutes.io</a>
|
||||
<a href="#" onclick="openAdoptionPanel();return false" style="color:var(--purple);border-bottom:2px solid var(--purple);padding-bottom:2px" title="ASPA & IPv6 Adoption Tracker">Adoption ✦</a>
|
||||
<span class="share-dropdown" id="shareDropdown">
|
||||
<a href="#" onclick="toggleShareMenu();return false" id="shareNavLink" title="Share" style="display:flex;align-items:center;gap:.35rem">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
|
||||
@ -556,7 +569,7 @@ body.dark .card{border-top-color:#e8e4dc}
|
||||
</section>
|
||||
|
||||
<!-- ASPA Status -->
|
||||
<section class="card" id="aspaCard" title="ASPA (Autonomous System Provider Authorization) status — RFC 9582. Verifies whether this AS has published which providers are authorized to forward its routes, protecting against route leaks">
|
||||
<section class="card" id="aspaCard" title="ASPA (Autonomous System Provider Authorization) status. Still an IETF draft (draft-ietf-sidrops-aspa-profile), not yet an RFC. Verifies whether this AS has published which providers are authorized to forward its routes, protecting against route leaks">
|
||||
<div class="card-title">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2v-4M9 21H5a2 2 0 0 1-2-2v-4"/></svg>
|
||||
ASPA Status
|
||||
@ -813,7 +826,7 @@ body.dark .card{border-top-color:#e8e4dc}
|
||||
|
||||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:0;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
|
||||
<div style="width:36px;height:36px;border-radius:0;background:rgba(180,83,9,.08);border:1px solid rgba(180,83,9,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem">🔐</div>
|
||||
<div><div style="font-weight:700;font-size:.85rem;color:var(--orange);font-family:var(--body)">ASPA (RFC 9582)</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem;font-family:var(--body)">AS Provider Authorization from Cloudflare RPKI JSON feed. RFC-compliant upstream/downstream path verification with valley detection.</div><a href="https://www.ietf.org/archive/id/draft-ietf-sidrops-aspa-verification-14.html" target="_blank" style="font-size:.65rem;color:var(--blue);font-family:var(--mono)">IETF Draft-14</a></div>
|
||||
<div><div style="font-weight:700;font-size:.85rem;color:var(--orange);font-family:var(--body)">ASPA (IETF draft, not yet an RFC)</div><div style="font-size:.7rem;color:var(--muted);margin-top:.15rem;font-family:var(--body)">AS Provider Authorization from Cloudflare RPKI JSON feed. Draft-compliant upstream/downstream path verification with valley detection.</div><a href="https://datatracker.ietf.org/doc/draft-ietf-sidrops-aspa-verification/" target="_blank" style="font-size:.65rem;color:var(--blue);font-family:var(--mono)">IETF Draft</a></div>
|
||||
</div>
|
||||
|
||||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:0;padding:1rem;display:flex;gap:.75rem;align-items:flex-start">
|
||||
@ -932,6 +945,7 @@ body.dark .card{border-top-color:#e8e4dc}
|
||||
<a href="https://stat.ripe.net" target="_blank">RIPE Stat</a>
|
||||
<a href="https://atlas.ripe.net" target="_blank">RIPE Atlas</a>
|
||||
<a href="https://bgproutes.io" target="_blank">bgproutes.io</a>
|
||||
<a href="#" onclick="openAdoptionPanel();return false" style="color:var(--purple);border-bottom:2px solid var(--purple);padding-bottom:2px" title="ASPA & IPv6 Adoption Tracker">Adoption ✦</a>
|
||||
<a href="https://github.com/renefichtmueller/PeerCortex" target="_blank">GitHub</a>
|
||||
</nav>
|
||||
<div class="ed-footer-copy">v0.6.9 · Open Source · MIT License · Data powered by PeeringDB · RIPE Stat · RIPE Atlas · Route Views · bgp.he.net · bgproutes.io · RIPE DB · Cloudflare RPKI</div>
|
||||
@ -1459,6 +1473,17 @@ function renderDashboard(d) {
|
||||
if (n.looking_glass) ov += '<a class="ext-link" href="' + escAttr(n.looking_glass) + '" target="_blank">Looking Glass</a>';
|
||||
if (n.route_server) ov += '<a class="ext-link" href="' + escAttr(n.route_server) + '" target="_blank">Route Server</a>';
|
||||
ov += '<a class="ext-link" href="#" onclick="exportRawJson(event)" title="Download full lookup data as JSON">⬇ Raw JSON</a>';
|
||||
ov += '<span style="display:inline-block;vertical-align:middle;margin:0 .5rem">';
|
||||
ov += '<span style="font-family:var(--mono);font-size:.58rem;color:var(--muted);letter-spacing:.06em;font-style:italic">Addon only for Izzy</span>';
|
||||
ov += '</span>';
|
||||
ov += '<span class="pdf-btn-wrap" style="position:relative;display:inline-block">';
|
||||
ov += '<a class="ext-link" href="#" onclick="togglePdfMenu(event)" style="color:var(--purple)">⬇ PDF ▾</a>';
|
||||
ov += '<div id="pdfMenu" style="display:none;position:absolute;top:1.6rem;left:0;background:var(--bg);border:1px solid var(--border);z-index:9000;white-space:nowrap;min-width:200px;box-shadow:0 4px 16px rgba(0,0,0,.2)">';
|
||||
ov += '<a href="/api/export/pdf?asn=' + n.asn + '&format=report" target="_blank" class="pdf-menu-item">Full Report (20+ pages)</a>';
|
||||
ov += '<a href="/api/export/pdf?asn=' + n.asn + '&format=executive" target="_blank" class="pdf-menu-item">Executive Summary (3-5 pages)</a>';
|
||||
ov += '<a href="/api/export/pdf?asn=' + n.asn + '&format=technical" target="_blank" class="pdf-menu-item">Technical Deep-Dive (10-15 pages)</a>';
|
||||
ov += '</div>';
|
||||
ov += '</span>';
|
||||
ov += '</div>';
|
||||
$('overviewContent').innerHTML = ov;
|
||||
|
||||
@ -2056,6 +2081,9 @@ async function doCompare() {
|
||||
}
|
||||
|
||||
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.5rem">Compared in ' + d.meta.duration_ms + 'ms</div>';
|
||||
h += '<div style="margin-top:.75rem;padding-top:.75rem;border-top:1px solid var(--border);text-align:right">';
|
||||
h += '<a href="/api/export/pdf/compare?asn1=' + currentAsn + '&asn2=' + raw2 + '" target="_blank" style="display:inline-block;padding:.45rem 1rem;background:var(--purple);color:#fff;font-family:var(--mono);font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;text-decoration:none" title="Download comparison as PDF">⬇ Download Comparison PDF</a>';
|
||||
h += '</div>';
|
||||
$('compareResults').innerHTML = h;
|
||||
} catch (e) {
|
||||
$('compareResults').innerHTML = '<div style="color:var(--red);font-size:.8rem">Compare failed: ' + escHtml(e.message) + '</div>';
|
||||
@ -2699,6 +2727,10 @@ function renderFullCompare(d) {
|
||||
|
||||
h += '</div>';
|
||||
h += '<div style="font-size:.7rem;color:var(--dim);margin-top:.75rem">Compared in ' + d.meta.duration_ms + 'ms</div>';
|
||||
h += '<div style="margin-top:1rem;padding-top:.75rem;border-top:1px solid var(--border);display:flex;align-items:center;gap:.75rem">';
|
||||
h += '<span style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.06em;font-style:italic">Addon only for Izzy</span>';
|
||||
h += '<a href="/api/export/pdf/compare?asn1=' + d.asn1.asn + '&asn2=' + d.asn2.asn + '" target="_blank" style="display:inline-block;padding:.4rem 1rem;background:var(--purple);color:#fff;font-family:var(--mono);font-size:.68rem;letter-spacing:.06em;text-transform:uppercase;text-decoration:none">⬇ Download Comparison PDF</a>';
|
||||
h += '</div>';
|
||||
h += '</div>'; // end card
|
||||
|
||||
panel.innerHTML = h;
|
||||
@ -4317,6 +4349,18 @@ function renderSourceTiming(d) {
|
||||
}
|
||||
|
||||
// ── Raw JSON Export ────────────────────────────────────────────
|
||||
function togglePdfMenu(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var menu = document.getElementById('pdfMenu');
|
||||
if (!menu) return;
|
||||
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
document.addEventListener('click', function() {
|
||||
var menu = document.getElementById('pdfMenu');
|
||||
if (menu) menu.style.display = 'none';
|
||||
});
|
||||
|
||||
function exportRawJson(e) {
|
||||
e.preventDefault();
|
||||
if (!window._lastLookupData) { alert('No lookup data available.'); return; }
|
||||
@ -4417,5 +4461,314 @@ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeChangel
|
||||
<div id="changelogContent" style="padding:0 2rem 2rem;max-height:72vh;overflow-y:auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ADOPTION TRACKER OVERLAY -->
|
||||
<div id="adoptionOverlay" style="display:none;position:fixed;inset:0;background:rgba(28,25,23,.6);z-index:9999;align-items:flex-start;justify-content:center;padding:2rem 1rem;overflow-y:auto;backdrop-filter:blur(2px)">
|
||||
<div style="background:var(--bg);max-width:900px;width:100%;border-top:3px solid var(--text);position:relative">
|
||||
<div style="padding:1.5rem 2rem 1rem;border-bottom:1px solid var(--border);display:flex;align-items:baseline;justify-content:space-between">
|
||||
<div>
|
||||
<div style="font-family:var(--serif);font-size:1.6rem;font-weight:900;letter-spacing:-.02em;line-height:1">Adoption Tracker</div>
|
||||
<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.1em;margin-top:.2rem">GLOBAL INTERNET ADOPTION METRICS</div>
|
||||
</div>
|
||||
<button onclick="closeAdoptionPanel()" style="background:none;border:none;color:var(--muted);cursor:pointer;font-family:var(--mono);font-size:.65rem;letter-spacing:.08em;text-transform:uppercase;padding:.3rem .5rem">Close ✕</button>
|
||||
</div>
|
||||
<div style="display:flex;border-bottom:1px solid var(--border);padding:0 2rem">
|
||||
<button id="adoptTab-aspa" onclick="switchAdoptionTab('aspa')" style="background:none;border:none;border-bottom:2px solid var(--purple);color:var(--text);cursor:pointer;font-family:var(--mono);font-size:.65rem;letter-spacing:.08em;text-transform:uppercase;padding:.75rem 1.2rem .65rem;margin-bottom:-1px">ASPA Adoption</button>
|
||||
<button id="adoptTab-ipv6" onclick="switchAdoptionTab('ipv6')" style="background:none;border:none;border-bottom:2px solid transparent;color:var(--muted);cursor:pointer;font-family:var(--mono);font-size:.65rem;letter-spacing:.08em;text-transform:uppercase;padding:.75rem 1.2rem .65rem;margin-bottom:-1px">IPv6 per RIR</button>
|
||||
</div>
|
||||
<div style="padding:1.5rem 2rem 2rem;max-height:75vh;overflow-y:auto">
|
||||
<div id="adoptPanel-aspa">
|
||||
<div id="aspaAdoptContent"><p style="font-family:var(--mono);font-size:.75rem;color:var(--muted);text-align:center;padding:2rem">Loading ASPA data…</p></div>
|
||||
</div>
|
||||
<div id="adoptPanel-ipv6" style="display:none">
|
||||
<div id="ipv6AdoptContent"><p style="font-family:var(--mono);font-size:.75rem;color:var(--muted);text-align:center;padding:2rem">Loading IPv6 data…</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var _adoptionLoaded = false;
|
||||
|
||||
window.openAdoptionPanel = function() {
|
||||
var overlay = document.getElementById('adoptionOverlay');
|
||||
overlay.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
if (!_adoptionLoaded) { _adoptionLoaded = true; loadAllAdoptionData(); }
|
||||
};
|
||||
|
||||
window.closeAdoptionPanel = function() {
|
||||
document.getElementById('adoptionOverlay').style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
|
||||
document.getElementById('adoptionOverlay').addEventListener('click', function(e) {
|
||||
if (e.target === this) window.closeAdoptionPanel();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') window.closeAdoptionPanel();
|
||||
});
|
||||
|
||||
window.switchAdoptionTab = function(tab) {
|
||||
['aspa','ipv6'].forEach(function(t) {
|
||||
var btn = document.getElementById('adoptTab-' + t);
|
||||
var panel = document.getElementById('adoptPanel-' + t);
|
||||
var active = t === tab;
|
||||
btn.style.borderBottomColor = active ? 'var(--purple)' : 'transparent';
|
||||
btn.style.color = active ? 'var(--text)' : 'var(--muted)';
|
||||
panel.style.display = active ? 'block' : 'none';
|
||||
});
|
||||
};
|
||||
|
||||
function loadAllAdoptionData() {
|
||||
loadAspaAdoptionData();
|
||||
loadIpv6AdoptionData();
|
||||
}
|
||||
|
||||
/* ── ASPA ───────────────────────────────────────────────── */
|
||||
function loadAspaAdoptionData() {
|
||||
var el = document.getElementById('aspaAdoptContent');
|
||||
fetch('/api/aspa-adoption-stats?period=90d')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
el.innerHTML = renderAspaAdoption(d);
|
||||
setTimeout(function() { drawAspaChart(d); }, 60);
|
||||
})
|
||||
.catch(function() {
|
||||
el.innerHTML = '<p style="font-family:var(--mono);font-size:.75rem;color:#b83a1b;text-align:center;padding:2rem">Failed to load ASPA data</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderAspaAdoption(d) {
|
||||
var snaps = d.trend || [];
|
||||
var latestSnap = snaps.length ? snaps[snaps.length - 1] : {};
|
||||
var pct = (d.current || {}).coverage_pct_atlas || 0; var current = d.current || {};
|
||||
var atlasTotal = current.atlas_asns_total || 0;
|
||||
var aspaCnt = current.atlas_asns_with_aspa || 0;
|
||||
var totalAspa = current.aspa_objects || 0;
|
||||
var firstSnap = snaps.length > 1 ? snaps[0] : null; var currentDate = (d.current || {}).date || '';
|
||||
var delta = firstSnap ? Math.round((pct - (firstSnap.coverage_pct_atlas || 0)) * 100) / 100 : 0;
|
||||
var trend = delta >= 0 ? '+' + delta : '' + delta;
|
||||
var deltaColor = delta >= 0 ? '#2d6a4f' : '#b83a1b';
|
||||
|
||||
var rows = snaps.slice().reverse().slice(0, 15).map(function(s) {
|
||||
return '<tr style="border-bottom:1px solid var(--border)">'
|
||||
+ '<td style="padding:.35rem .5rem">' + s.date + '</td>'
|
||||
+ '<td style="text-align:right;padding:.35rem .5rem;color:var(--purple);font-weight:700">' + s.coverage_pct_atlas + '%</td>'
|
||||
+ '<td style="text-align:right;padding:.35rem .5rem">' + (s.atlas_asns_with_aspa||0) + '</td>'
|
||||
+ '<td style="text-align:right;padding:.35rem .5rem">' + (s.atlas_asns_total||0) + '</td>'
|
||||
+ '<td style="text-align:right;padding:.35rem .5rem">' + (s.aspa_objects||0).toLocaleString() + '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
|
||||
return '<div style="margin-bottom:1.5rem">'
|
||||
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;margin-bottom:1.5rem">'
|
||||
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
|
||||
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.3rem">Atlas Coverage</div>'
|
||||
+ '<div style="font-family:var(--serif);font-size:2rem;font-weight:900;letter-spacing:-.03em;color:var(--purple)">' + pct + '%</div>'
|
||||
+ '<div style="font-family:var(--mono);font-size:.65rem;color:var(--muted);margin-top:.25rem">' + aspaCnt + ' / ' + atlasTotal + ' ASNs</div>'
|
||||
+ '</div>'
|
||||
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
|
||||
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.3rem">ASPA Objects</div>'
|
||||
+ '<div style="font-family:var(--serif);font-size:2rem;font-weight:900;letter-spacing:-.03em">' + totalAspa.toLocaleString() + '</div>'
|
||||
+ '<div style="font-family:var(--mono);font-size:.65rem;color:var(--muted);margin-top:.25rem">in RPKI</div>'
|
||||
+ '</div>'
|
||||
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
|
||||
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.3rem">90d Change</div>'
|
||||
+ '<div style="font-family:var(--serif);font-size:2rem;font-weight:900;letter-spacing:-.03em;color:' + deltaColor + '">' + trend + '%</div>'
|
||||
+ '<div style="font-family:var(--mono);font-size:.65rem;color:var(--muted);margin-top:.25rem">vs. 90 days ago</div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem;margin-bottom:1rem">'
|
||||
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.75rem">Atlas Coverage Trend (' + (d.period||'90d') + ')</div>'
|
||||
+ '<canvas id="aspaChartCanvas" height="160" style="width:100%;display:block"></canvas>'
|
||||
+ '</div>'
|
||||
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
|
||||
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.75rem">Recent Snapshots</div>'
|
||||
+ '<table style="width:100%;border-collapse:collapse;font-family:var(--mono);font-size:.72rem">'
|
||||
+ '<thead><tr style="border-bottom:1px solid var(--border)">'
|
||||
+ '<th style="text-align:left;padding:.35rem .5rem;color:var(--muted);font-weight:400">Date</th>'
|
||||
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">Coverage %</th>'
|
||||
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">ASNs w/ ASPA</th>'
|
||||
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">Total Atlas</th>'
|
||||
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">ASPA Objects</th>'
|
||||
+ '</tr></thead>'
|
||||
+ '<tbody>' + rows + '</tbody>'
|
||||
+ '</table>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
function drawAspaChart(d) {
|
||||
var canvas = document.getElementById('aspaChartCanvas');
|
||||
if (!canvas) return;
|
||||
var snaps = d.snapshots || [];
|
||||
if (snaps.length < 2) { canvas.style.display = 'none'; return; }
|
||||
var pts = snaps.map(function(s) { return s.coverage_pct_atlas; });
|
||||
var W = canvas.offsetWidth || 800;
|
||||
canvas.width = W;
|
||||
var H = 160;
|
||||
canvas.height = H;
|
||||
var ctx = canvas.getContext('2d');
|
||||
var minV = Math.max(0, Math.min.apply(null, pts) - 1);
|
||||
var maxV = Math.max.apply(null, pts) + 1;
|
||||
var pad = { t:10, b:24, l:40, r:10 };
|
||||
var w = W - pad.l - pad.r, h = H - pad.t - pad.b;
|
||||
var xf = function(i) { return pad.l + (i / (pts.length - 1)) * w; };
|
||||
var yf = function(v) { return pad.t + h - ((v - minV) / (maxV - minV)) * h; };
|
||||
var st = getComputedStyle(document.documentElement);
|
||||
var bc = st.getPropertyValue('--border').trim() || '#C9C3B6';
|
||||
var mc = st.getPropertyValue('--muted').trim() || '#8a8278';
|
||||
ctx.strokeStyle = bc; ctx.lineWidth = 1;
|
||||
for (var tick = 0; tick <= 4; tick++) {
|
||||
var yy = pad.t + (tick / 4) * h;
|
||||
ctx.beginPath(); ctx.moveTo(pad.l, yy); ctx.lineTo(pad.l + w, yy); ctx.stroke();
|
||||
ctx.fillStyle = mc; ctx.font = '10px monospace'; ctx.textAlign = 'right';
|
||||
ctx.fillText((maxV - (tick / 4) * (maxV - minV)).toFixed(1) + '%', pad.l - 4, yy + 4);
|
||||
}
|
||||
ctx.beginPath(); ctx.moveTo(xf(0), yf(pts[0]));
|
||||
for (var i2 = 1; i2 < pts.length; i2++) ctx.lineTo(xf(i2), yf(pts[i2]));
|
||||
ctx.lineTo(xf(pts.length - 1), H - pad.b); ctx.lineTo(xf(0), H - pad.b);
|
||||
ctx.closePath(); ctx.fillStyle = 'rgba(184,58,27,.12)'; ctx.fill();
|
||||
ctx.beginPath(); ctx.moveTo(xf(0), yf(pts[0]));
|
||||
for (var i3 = 1; i3 < pts.length; i3++) ctx.lineTo(xf(i3), yf(pts[i3]));
|
||||
ctx.strokeStyle = '#B83A1B'; ctx.lineWidth = 2; ctx.stroke();
|
||||
for (var i4 = 0; i4 < pts.length; i4++) {
|
||||
ctx.beginPath(); ctx.arc(xf(i4), yf(pts[i4]), 3, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#B83A1B'; ctx.fill();
|
||||
}
|
||||
ctx.fillStyle = mc; ctx.font = '9px monospace'; ctx.textAlign = 'center';
|
||||
var step = Math.max(1, Math.ceil(pts.length / 6));
|
||||
for (var i5 = 0; i5 < snaps.length; i5++) {
|
||||
if (i5 % step === 0 || i5 === snaps.length - 1)
|
||||
ctx.fillText(snaps[i5].date.slice(5), xf(i5), H - pad.b + 14);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── IPv6 ───────────────────────────────────────────────── */
|
||||
var RIR_COLORS = { RIPE:'#4361ee', ARIN:'#3a0ca3', APNIC:'#7209b7', AFRINIC:'#f72585', LACNIC:'#4cc9f0' };
|
||||
|
||||
function loadIpv6AdoptionData() {
|
||||
var el = document.getElementById('ipv6AdoptContent');
|
||||
fetch('/api/ipv6-adoption-stats')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
el.innerHTML = renderIpv6Adoption(d);
|
||||
setTimeout(function() { drawIpv6Chart(d); }, 60);
|
||||
})
|
||||
.catch(function() {
|
||||
el.innerHTML = '<p style="font-family:var(--mono);font-size:.75rem;color:#b83a1b;text-align:center;padding:2rem">Failed to load IPv6 data</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderIpv6Adoption(d) {
|
||||
var rirs = d.rirs || [];
|
||||
var global = d.global || {};
|
||||
var fetchedAt = d.fetched_at ? new Date(d.fetched_at).toLocaleDateString() : 'N/A';
|
||||
|
||||
var statCards = rirs.map(function(r) {
|
||||
return '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
|
||||
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.3rem">' + r.rir + '</div>'
|
||||
+ '<div style="font-family:var(--serif);font-size:1.8rem;font-weight:900;letter-spacing:-.03em;color:' + (RIR_COLORS[r.rir] || 'var(--text)') + '">' + r.ipv6_pct + '%</div>'
|
||||
+ '<div style="font-family:var(--mono);font-size:.65rem;color:var(--muted);margin-top:.25rem">' + (r.ipv6_blocks||0).toLocaleString() + ' / ' + (r.total_blocks||0).toLocaleString() + ' blocks</div>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
|
||||
var tableRows = rirs.map(function(r) {
|
||||
return '<tr style="border-bottom:1px solid var(--border)">'
|
||||
+ '<td style="padding:.35rem .5rem;font-weight:700">' + r.rir + '</td>'
|
||||
+ '<td style="text-align:right;padding:.35rem .5rem;color:' + (RIR_COLORS[r.rir]||'var(--text)') + ';font-weight:700">' + r.ipv6_pct + '%</td>'
|
||||
+ '<td style="text-align:right;padding:.35rem .5rem">' + (r.ipv6_blocks||0).toLocaleString() + '</td>'
|
||||
+ '<td style="text-align:right;padding:.35rem .5rem">' + (r.ipv4_blocks||0).toLocaleString() + '</td>'
|
||||
+ '<td style="text-align:right;padding:.35rem .5rem">' + (r.asn_records||0).toLocaleString() + '</td>'
|
||||
+ '<td style="text-align:right;padding:.35rem .5rem">' + (r.total_blocks||0).toLocaleString() + '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
|
||||
tableRows += '<tr style="border-top:2px solid var(--border);font-weight:700">'
|
||||
+ '<td style="padding:.4rem .5rem">Global</td>'
|
||||
+ '<td style="text-align:right;padding:.4rem .5rem;color:var(--purple)">' + (global.ipv6_pct||0) + '%</td>'
|
||||
+ '<td style="text-align:right;padding:.4rem .5rem">' + (global.ipv6_blocks||0).toLocaleString() + '</td>'
|
||||
+ '<td style="text-align:right;padding:.4rem .5rem">' + (global.ipv4_blocks||0).toLocaleString() + '</td>'
|
||||
+ '<td style="text-align:right;padding:.4rem .5rem">' + (global.asn_records||0).toLocaleString() + '</td>'
|
||||
+ '<td style="text-align:right;padding:.4rem .5rem">' + (global.total_blocks||0).toLocaleString() + '</td>'
|
||||
+ '</tr>';
|
||||
|
||||
return '<div>'
|
||||
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin-bottom:1.5rem">'
|
||||
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
|
||||
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.3rem">Global IPv6 Rate</div>'
|
||||
+ '<div style="font-family:var(--serif);font-size:2rem;font-weight:900;letter-spacing:-.03em;color:var(--purple)">' + (global.ipv6_pct||0) + '%</div>'
|
||||
+ '<div style="font-family:var(--mono);font-size:.65rem;color:var(--muted);margin-top:.25rem">of all address blocks</div>'
|
||||
+ '</div>'
|
||||
+ statCards
|
||||
+ '</div>'
|
||||
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem;margin-bottom:1rem">'
|
||||
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.75rem">IPv6 Adoption by RIR</div>'
|
||||
+ '<canvas id="ipv6ChartCanvas" height="180" style="width:100%;display:block"></canvas>'
|
||||
+ '</div>'
|
||||
+ '<div style="border:1px solid var(--border);padding:1rem 1.25rem">'
|
||||
+ '<div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:.75rem">Breakdown by RIR</div>'
|
||||
+ '<table style="width:100%;border-collapse:collapse;font-family:var(--mono);font-size:.72rem">'
|
||||
+ '<thead><tr style="border-bottom:1px solid var(--border)">'
|
||||
+ '<th style="text-align:left;padding:.35rem .5rem;color:var(--muted);font-weight:400">RIR</th>'
|
||||
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">IPv6 %</th>'
|
||||
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">IPv6 Blocks</th>'
|
||||
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">IPv4 Blocks</th>'
|
||||
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">ASN Records</th>'
|
||||
+ '<th style="text-align:right;padding:.35rem .5rem;color:var(--muted);font-weight:400">Total</th>'
|
||||
+ '</tr></thead>'
|
||||
+ '<tbody>' + tableRows + '</tbody>'
|
||||
+ '</table>'
|
||||
+ '<p style="font-family:var(--mono);font-size:.6rem;color:var(--muted);margin-top:.75rem">Source: RIR delegation files (RIPE, ARIN, APNIC, AFRINIC, LACNIC) · Updated: ' + fetchedAt + '</p>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
function drawIpv6Chart(d) {
|
||||
var canvas = document.getElementById('ipv6ChartCanvas');
|
||||
if (!canvas) return;
|
||||
var rirs = d.rirs || [];
|
||||
if (!rirs.length) return;
|
||||
var W = canvas.offsetWidth || 800;
|
||||
canvas.width = W;
|
||||
var H = 180;
|
||||
canvas.height = H;
|
||||
var ctx = canvas.getContext('2d');
|
||||
var pad = { t:10, b:32, l:42, r:10 };
|
||||
var w = W - pad.l - pad.r, h = H - pad.t - pad.b;
|
||||
var maxPct = Math.ceil(Math.max.apply(null, rirs.map(function(r) { return r.ipv6_pct; })) / 5) * 5 + 5;
|
||||
var barW = Math.floor(w / rirs.length * 0.55);
|
||||
var gap = w / rirs.length;
|
||||
var st2 = getComputedStyle(document.documentElement);
|
||||
var bc2 = st2.getPropertyValue('--border').trim() || '#C9C3B6';
|
||||
var mc2 = st2.getPropertyValue('--muted').trim() || '#8a8278';
|
||||
ctx.strokeStyle = bc2; ctx.lineWidth = 1;
|
||||
for (var tick = 0; tick <= 4; tick++) {
|
||||
var yy = pad.t + (tick / 4) * h;
|
||||
ctx.beginPath(); ctx.moveTo(pad.l, yy); ctx.lineTo(pad.l + w, yy); ctx.stroke();
|
||||
ctx.fillStyle = mc2; ctx.font = '10px monospace'; ctx.textAlign = 'right';
|
||||
ctx.fillText((maxPct - (tick / 4) * maxPct).toFixed(0) + '%', pad.l - 4, yy + 4);
|
||||
}
|
||||
rirs.forEach(function(r, i) {
|
||||
var barH = (r.ipv6_pct / maxPct) * h;
|
||||
var bx = pad.l + gap * i + (gap - barW) / 2;
|
||||
var by = pad.t + h - barH;
|
||||
ctx.fillStyle = RIR_COLORS[r.rir] || '#666';
|
||||
ctx.fillRect(bx, by, barW, barH);
|
||||
if (barH > 18) {
|
||||
ctx.fillStyle = '#fff'; ctx.font = 'bold 11px monospace'; ctx.textAlign = 'center';
|
||||
ctx.fillText(r.ipv6_pct + '%', bx + barW / 2, by + barH / 2 + 4);
|
||||
}
|
||||
ctx.fillStyle = mc2; ctx.font = '10px monospace'; ctx.textAlign = 'center';
|
||||
ctx.fillText(r.rir, bx + barW / 2, H - pad.b + 14);
|
||||
});
|
||||
}
|
||||
|
||||
})(); // end IIFE
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
4421
public/index.html.bak-20260429_211603
Normal file
4421
public/index.html.bak-20260429_211603
Normal file
File diff suppressed because it is too large
Load Diff
1129
public/public/design-draft-editorial.html
Normal file
1129
public/public/design-draft-editorial.html
Normal file
File diff suppressed because it is too large
Load Diff
2898
public/public/index-classic.html
Normal file
2898
public/public/index-classic.html
Normal file
File diff suppressed because it is too large
Load Diff
3332
public/public/index-editorial.html
Normal file
3332
public/public/index-editorial.html
Normal file
File diff suppressed because it is too large
Load Diff
4126
public/public/index.html
Normal file
4126
public/public/index.html
Normal file
File diff suppressed because it is too large
Load Diff
2357
public/public/index.html.bak
Normal file
2357
public/public/index.html.bak
Normal file
File diff suppressed because it is too large
Load Diff
488
public/public/lia.html
Normal file
488
public/public/lia.html
Normal file
@ -0,0 +1,488 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Lia's Paradise — RIPE Atlas Coverage Explorer</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--bg:#0f0f1a;--card:#1a1b26;--border:#2a2b3d;--border-light:#363750;--text:#e2e8f0;--text-dim:#94a3b8;--muted:#64748b;--dim:#475569;--purple:#a78bfa;--blue:#60a5fa;--green:#4ade80;--orange:#fbbf24;--red:#f87171;--cyan:#22d3ee;--pink:#f472b6}
|
||||
body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
||||
.header{background:linear-gradient(135deg,#1a1b26 0%,#1e1f30 100%);border-bottom:1px solid var(--border);padding:1.5rem 2rem;text-align:center}
|
||||
.header h1{font-size:1.8rem;font-weight:800;background:linear-gradient(135deg,var(--pink),var(--purple),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:.25rem}
|
||||
.header p{color:var(--text-dim);font-size:.85rem}
|
||||
.easter-egg{font-size:.65rem;color:var(--dim);margin-top:.25rem;font-style:italic}
|
||||
.controls{max-width:1400px;margin:1.5rem auto;padding:0 1.5rem;display:flex;gap:1rem;flex-wrap:wrap;align-items:center}
|
||||
.search-box{flex:1;min-width:200px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:.7rem 1rem;font-size:.9rem;color:var(--text);font-family:inherit;outline:none}
|
||||
.search-box:focus{border-color:var(--purple)}
|
||||
.search-box::placeholder{color:var(--dim)}
|
||||
.rir-tabs{display:flex;gap:.4rem;flex-wrap:wrap}
|
||||
.rir-tab{padding:.5rem 1rem;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text-dim);font-size:.8rem;font-weight:600;cursor:pointer;transition:all .2s;font-family:inherit}
|
||||
.rir-tab:hover{border-color:var(--purple);color:var(--text)}
|
||||
.rir-tab.active{background:linear-gradient(135deg,#5b21b6,#7c3aed);color:#fff;border-color:#7c3aed}
|
||||
.export-btn{padding:.5rem 1.2rem;border-radius:8px;border:1px solid var(--cyan);background:transparent;color:var(--cyan);font-size:.8rem;font-weight:600;cursor:pointer;transition:all .2s;font-family:inherit}
|
||||
.export-btn:hover{background:var(--cyan);color:var(--bg)}
|
||||
.stats-bar{max-width:1400px;margin:.75rem auto;padding:0 1.5rem;display:flex;gap:1.5rem;flex-wrap:wrap}
|
||||
.stat-pill{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:.5rem 1rem;display:flex;align-items:center;gap:.5rem}
|
||||
.stat-pill .num{font-size:1.1rem;font-weight:800}
|
||||
.stat-pill .label{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
|
||||
.content{max-width:1400px;margin:1rem auto;padding:0 1.5rem}
|
||||
.loading{text-align:center;padding:3rem;color:var(--muted);font-size:.9rem}
|
||||
.loading .spinner{display:inline-block;width:20px;height:20px;border:2px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;margin-right:.5rem;vertical-align:middle}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.country-section{margin-bottom:1.5rem}
|
||||
.country-header{display:flex;align-items:center;gap:.75rem;padding:.6rem 1rem;background:var(--card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .2s;margin-bottom:.5rem}
|
||||
.country-header:hover{border-color:var(--purple)}
|
||||
.country-flag{font-size:1.2rem}
|
||||
.country-name{font-weight:700;font-size:.95rem;flex:1}
|
||||
.country-count{font-size:.75rem;color:var(--muted)}
|
||||
.country-badge{padding:.2rem .6rem;border-radius:6px;font-size:.7rem;font-weight:700}
|
||||
.badge-red{background:#f8717118;color:var(--red);border:1px solid #f8717130}
|
||||
.badge-green{background:#4ade8018;color:var(--green);border:1px solid #4ade8030}
|
||||
.badge-orange{background:#fbbf2418;color:var(--orange);border:1px solid #fbbf2430}
|
||||
.asn-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:.4rem;padding:0 0 .5rem 0}
|
||||
.asn-row{display:flex;align-items:center;gap:.65rem;padding:.5rem .85rem;background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:.8rem;transition:all .15s;cursor:pointer}
|
||||
.asn-row:hover{border-color:var(--purple);transform:translateY(-1px)}
|
||||
.asn-num{font-weight:700;color:var(--purple);min-width:80px;font-family:'JetBrains Mono',monospace}
|
||||
.asn-name{flex:1;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.asn-type{font-size:.65rem;padding:.15rem .4rem;border-radius:4px;background:var(--card);color:var(--dim);border:1px solid var(--border)}
|
||||
.no-probe{color:var(--red);font-size:.65rem;font-weight:600}
|
||||
.has-probe{color:var(--green);font-size:.65rem;font-weight:600}
|
||||
.summary-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:.75rem;margin-bottom:1.5rem}
|
||||
.summary-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.25rem;text-align:center}
|
||||
.summary-card .rir-name{font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.1em;margin-bottom:.5rem}
|
||||
.summary-card .big-num{font-size:2rem;font-weight:800}
|
||||
.summary-card .sub{font-size:.7rem;color:var(--dim);margin-top:.25rem}
|
||||
.progress-bar{height:6px;background:var(--bg);border-radius:3px;margin-top:.5rem;overflow:hidden}
|
||||
.progress-fill{height:100%;border-radius:3px;transition:width .5s ease}
|
||||
.hidden{display:none}
|
||||
.footer{text-align:center;padding:2rem;color:var(--dim);font-size:.7rem;margin-top:2rem;border-top:1px solid var(--border)}
|
||||
.footer a{color:var(--purple);text-decoration:none}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Lia's Paradise</h1>
|
||||
<p>RIPE Atlas Coverage Explorer — Networks without Atlas Probes, Anchors & Software Probes</p>
|
||||
<div class="easter-egg">For Lia, who makes the Internet measurable — one probe at a time</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<input type="text" class="search-box" id="searchInput" placeholder="Search by country, ASN, or network name..." oninput="filterResults()">
|
||||
<div class="rir-tabs" id="rirTabs">
|
||||
<button class="rir-tab active" onclick="switchRIR('all',this)">All RIRs</button>
|
||||
<button class="rir-tab" onclick="switchRIR('ripencc',this)">RIPE NCC</button>
|
||||
<button class="rir-tab" onclick="switchRIR('arin',this)">ARIN</button>
|
||||
<button class="rir-tab" onclick="switchRIR('apnic',this)">APNIC</button>
|
||||
<button class="rir-tab" onclick="switchRIR('lacnic',this)">LACNIC</button>
|
||||
<button class="rir-tab" onclick="switchRIR('afrinic',this)">AFRINIC</button>
|
||||
</div>
|
||||
<button class="export-btn" onclick="exportPDF()">Export PDF</button>
|
||||
</div>
|
||||
|
||||
<div style="max-width:1400px;margin:.75rem auto;padding:0 1.5rem">
|
||||
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1rem 1.5rem">
|
||||
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
||||
<div style="font-size:.8rem;font-weight:600;color:var(--pink)">FILE LOOKUP</div>
|
||||
<div style="font-size:.75rem;color:var(--dim)">Upload a file with ASNs or company names — we'll check probe coverage for each</div>
|
||||
<div style="flex:1"></div>
|
||||
<label style="padding:.45rem 1rem;border-radius:8px;border:1px solid var(--pink);background:transparent;color:var(--pink);font-size:.8rem;font-weight:600;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:.4rem" onmouseenter="this.style.background='var(--pink)';this.style.color='var(--bg)'" onmouseleave="this.style.background='transparent';this.style.color='var(--pink)'">
|
||||
<span>Upload File</span>
|
||||
<input type="file" id="fileUpload" accept=".csv,.txt,.pdf,.xls,.xlsx,.doc,.docx" style="display:none" onchange="handleFileUpload(this)">
|
||||
</label>
|
||||
<span id="fileStatus" style="font-size:.7rem;color:var(--dim)"></span>
|
||||
</div>
|
||||
<div id="fileResults" class="hidden" style="margin-top:1rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-bar" id="statsBar"></div>
|
||||
|
||||
<div id="summaryGrid" class="summary-grid" style="max-width:1400px;margin:1rem auto;padding:0 1.5rem"></div>
|
||||
|
||||
<div class="content" id="mainContent">
|
||||
<div class="loading"><span class="spinner"></span>Loading Atlas coverage data across all RIRs... This may take a moment.</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<a href="/">Back to PeerCortex</a> · Data from <a href="https://atlas.ripe.net" target="_blank">RIPE Atlas</a> & <a href="https://www.peeringdb.com" target="_blank">PeeringDB</a>
|
||||
<br>Made with love for the Atlas community
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var allData = [];
|
||||
var currentRIR = 'all';
|
||||
var countryFlags = {};
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
|
||||
// Country code to flag emoji
|
||||
function flag(cc) {
|
||||
if (!cc || cc.length !== 2) return '';
|
||||
return String.fromCodePoint(...cc.toUpperCase().split('').map(c => 0x1F1E6 + c.charCodeAt(0) - 65));
|
||||
}
|
||||
|
||||
// RIR mapping by country
|
||||
var rirMap = {
|
||||
ripencc: new Set(['AT','BE','BG','HR','CY','CZ','DK','EE','FI','FR','DE','GR','HU','IS','IE','IT','LV','LT','LU','MT','NL','NO','PL','PT','RO','SK','SI','ES','SE','CH','GB','UA','RU','TR','GE','AZ','AM','MD','BY','RS','BA','ME','MK','AL','XK','LI','MC','SM','VA','AD','FO','GL','AX','GG','JE','IM','BH','IQ','IR','IL','JO','KW','LB','OM','PS','QA','SA','SY','AE','YE','DZ','EG','LY','MA','TN','EH']),
|
||||
arin: new Set(['US','CA','PR','VI','GU','AS','MP','MH','FM','PW','UM']),
|
||||
apnic: new Set(['AU','NZ','JP','KR','CN','HK','MO','TW','IN','BD','PK','LK','NP','BT','MV','AF','MM','TH','VN','LA','KH','MY','SG','ID','PH','BN','TL','PG','FJ','WS','TO','VU','SB','KI','NR','TV','MN','KZ','KG','TJ','TM','UZ']),
|
||||
lacnic: new Set(['MX','GT','BZ','SV','HN','NI','CR','PA','CO','VE','EC','PE','BO','CL','AR','UY','PY','BR','GY','SR','GF','CU','JM','HT','DO','TT','BB','AG','DM','GD','KN','LC','VC','BS','CW','AW','SX','BQ','TC','KY','BM']),
|
||||
afrinic: new Set(['ZA','NG','KE','GH','TZ','UG','ET','RW','SN','CI','CM','CD','CG','GA','AO','MZ','ZW','ZM','MW','BW','NA','SZ','LS','MG','MU','SC','DJ','ER','SO','SD','SS','TD','NE','ML','BF','GW','GN','SL','LR','TG','BJ','CF','GQ']),
|
||||
};
|
||||
|
||||
function getRIR(cc) {
|
||||
if (!cc) return 'unknown';
|
||||
for (var r in rirMap) { if (rirMap[r].has(cc.toUpperCase())) return r; }
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
// Fetch PeeringDB networks with IX presence (active networks)
|
||||
var resp = await fetch('/api/lia/coverage');
|
||||
var d = await resp.json();
|
||||
if (d.error) throw new Error(d.error);
|
||||
allData = d.networks || [];
|
||||
renderAll();
|
||||
} catch (e) {
|
||||
$('mainContent').innerHTML = '<div class="loading" style="color:var(--red)">Failed to load: ' + escHtml(e.message) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
renderSummary();
|
||||
renderStats();
|
||||
renderCountries();
|
||||
}
|
||||
|
||||
function renderSummary() {
|
||||
var rir_stats = { ripencc: {total:0,noProbe:0}, arin: {total:0,noProbe:0}, apnic: {total:0,noProbe:0}, lacnic: {total:0,noProbe:0}, afrinic: {total:0,noProbe:0} };
|
||||
var rirLabels = { ripencc:'RIPE NCC', arin:'ARIN', apnic:'APNIC', lacnic:'LACNIC', afrinic:'AFRINIC' };
|
||||
var rirColors = { ripencc:'var(--blue)', arin:'var(--green)', apnic:'var(--orange)', lacnic:'var(--pink)', afrinic:'var(--cyan)' };
|
||||
|
||||
allData.forEach(function(n) {
|
||||
var r = getRIR(n.country);
|
||||
if (rir_stats[r]) {
|
||||
rir_stats[r].total++;
|
||||
if (!n.has_probe) rir_stats[r].noProbe++;
|
||||
}
|
||||
});
|
||||
|
||||
var h = '';
|
||||
for (var r in rir_stats) {
|
||||
var s = rir_stats[r];
|
||||
var covPct = s.total > 0 ? ((s.total - s.noProbe) / s.total * 100).toFixed(1) : '0';
|
||||
var col = rirColors[r];
|
||||
h += '<div class="summary-card">';
|
||||
h += '<div class="rir-name">' + rirLabels[r] + '</div>';
|
||||
h += '<div class="big-num" style="color:' + col + '">' + s.noProbe.toLocaleString() + '</div>';
|
||||
h += '<div class="sub">of ' + s.total.toLocaleString() + ' networks without a probe</div>';
|
||||
h += '<div class="progress-bar"><div class="progress-fill" style="width:' + covPct + '%;background:' + col + '"></div></div>';
|
||||
h += '<div class="sub" style="margin-top:.25rem">' + covPct + '% coverage</div>';
|
||||
h += '</div>';
|
||||
}
|
||||
$('summaryGrid').innerHTML = h;
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
var filtered = getFiltered();
|
||||
var noProbe = filtered.filter(function(n) { return !n.has_probe; }).length;
|
||||
var withProbe = filtered.length - noProbe;
|
||||
var countries = new Set(filtered.map(function(n) { return n.country; })).size;
|
||||
|
||||
var h = '';
|
||||
h += '<div class="stat-pill"><span class="num" style="color:var(--red)">' + noProbe.toLocaleString() + '</span><span class="label">Without Probe</span></div>';
|
||||
h += '<div class="stat-pill"><span class="num" style="color:var(--green)">' + withProbe.toLocaleString() + '</span><span class="label">With Probe</span></div>';
|
||||
h += '<div class="stat-pill"><span class="num" style="color:var(--purple)">' + filtered.length.toLocaleString() + '</span><span class="label">Total Networks</span></div>';
|
||||
h += '<div class="stat-pill"><span class="num" style="color:var(--cyan)">' + countries + '</span><span class="label">Countries</span></div>';
|
||||
$('statsBar').innerHTML = h;
|
||||
}
|
||||
|
||||
function getFiltered() {
|
||||
var q = ($('searchInput').value || '').toLowerCase();
|
||||
return allData.filter(function(n) {
|
||||
if (currentRIR !== 'all' && getRIR(n.country) !== currentRIR) return false;
|
||||
if (q) {
|
||||
var haystack = (n.name + ' ' + n.country + ' ' + n.country_name + ' AS' + n.asn + ' ' + (n.info_type || '')).toLowerCase();
|
||||
if (haystack.indexOf(q) < 0) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function renderCountries() {
|
||||
var filtered = getFiltered();
|
||||
|
||||
// Group by country
|
||||
var byCountry = {};
|
||||
filtered.forEach(function(n) {
|
||||
var cc = n.country || 'XX';
|
||||
if (!byCountry[cc]) byCountry[cc] = { name: n.country_name || cc, networks: [] };
|
||||
byCountry[cc].networks.push(n);
|
||||
});
|
||||
|
||||
// Sort countries by number of networks without probes (descending)
|
||||
var countries = Object.keys(byCountry).sort(function(a, b) {
|
||||
var aNo = byCountry[a].networks.filter(function(n) { return !n.has_probe; }).length;
|
||||
var bNo = byCountry[b].networks.filter(function(n) { return !n.has_probe; }).length;
|
||||
return bNo - aNo;
|
||||
});
|
||||
|
||||
var h = '';
|
||||
countries.forEach(function(cc) {
|
||||
var c = byCountry[cc];
|
||||
var noProbe = c.networks.filter(function(n) { return !n.has_probe; });
|
||||
var withProbe = c.networks.filter(function(n) { return n.has_probe; });
|
||||
var covPct = (withProbe.length / c.networks.length * 100).toFixed(0);
|
||||
var badgeClass = covPct >= 70 ? 'badge-green' : covPct >= 30 ? 'badge-orange' : 'badge-red';
|
||||
var secId = 'country_' + cc;
|
||||
|
||||
h += '<div class="country-section">';
|
||||
h += '<div class="country-header" onclick="var el=document.getElementById(\'' + secId + '\');el.classList.toggle(\'hidden\');this.querySelector(\'.arrow\').textContent=el.classList.contains(\'hidden\')?\'\u25B6\':\'\u25BC\'">';
|
||||
h += '<span class="country-flag">' + flag(cc) + '</span>';
|
||||
h += '<span class="country-name">' + escHtml(c.name) + ' (' + cc + ')</span>';
|
||||
h += '<span class="country-count">' + noProbe.length + ' without probe / ' + c.networks.length + ' total</span>';
|
||||
h += '<span class="' + badgeClass + ' country-badge">' + covPct + '% coverage</span>';
|
||||
h += '<span class="arrow" style="color:var(--muted)">\u25B6</span>';
|
||||
h += '</div>';
|
||||
|
||||
h += '<div id="' + secId + '" class="hidden">';
|
||||
// Show networks WITHOUT probes first
|
||||
if (noProbe.length > 0) {
|
||||
h += '<div style="font-size:.7rem;color:var(--red);font-weight:600;margin:.5rem 0 .3rem;padding-left:.5rem">NO PROBE (' + noProbe.length + ')</div>';
|
||||
h += '<div class="asn-grid">';
|
||||
noProbe.sort(function(a,b) { return a.asn - b.asn; }).forEach(function(n) {
|
||||
h += '<div class="asn-row" onclick="window.open(\'/?asn=' + n.asn + '\',\'_blank\')">';
|
||||
h += '<span class="asn-num">AS' + n.asn + '</span>';
|
||||
h += '<span class="asn-name">' + escHtml(n.name || '') + '</span>';
|
||||
if (n.info_type) h += '<span class="asn-type">' + escHtml(n.info_type) + '</span>';
|
||||
h += '<span class="no-probe">\u2718 No Probe</span>';
|
||||
h += '</div>';
|
||||
});
|
||||
h += '</div>';
|
||||
}
|
||||
// Networks WITH probes
|
||||
if (withProbe.length > 0) {
|
||||
h += '<div style="font-size:.7rem;color:var(--green);font-weight:600;margin:.75rem 0 .3rem;padding-left:.5rem">HAS PROBE (' + withProbe.length + ')</div>';
|
||||
h += '<div class="asn-grid">';
|
||||
withProbe.sort(function(a,b) { return a.asn - b.asn; }).slice(0, 20).forEach(function(n) {
|
||||
h += '<div class="asn-row" onclick="window.open(\'/?asn=' + n.asn + '\',\'_blank\')">';
|
||||
h += '<span class="asn-num">AS' + n.asn + '</span>';
|
||||
h += '<span class="asn-name">' + escHtml(n.name || '') + '</span>';
|
||||
if (n.info_type) h += '<span class="asn-type">' + escHtml(n.info_type) + '</span>';
|
||||
h += '<span class="has-probe">\u2714 Probe</span>';
|
||||
h += '</div>';
|
||||
});
|
||||
if (withProbe.length > 20) h += '<div style="font-size:.75rem;color:var(--dim);padding:.3rem .5rem">+ ' + (withProbe.length - 20) + ' more with probes</div>';
|
||||
h += '</div>';
|
||||
}
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
});
|
||||
|
||||
if (countries.length === 0) {
|
||||
h = '<div class="loading">No results found. Try a different search or RIR filter.</div>';
|
||||
}
|
||||
|
||||
$('mainContent').innerHTML = h;
|
||||
}
|
||||
|
||||
function switchRIR(rir, btn) {
|
||||
currentRIR = rir;
|
||||
document.querySelectorAll('.rir-tab').forEach(function(t) { t.classList.remove('active'); });
|
||||
btn.classList.add('active');
|
||||
renderStats();
|
||||
renderCountries();
|
||||
}
|
||||
|
||||
function filterResults() {
|
||||
renderStats();
|
||||
renderCountries();
|
||||
}
|
||||
|
||||
function exportPDF() {
|
||||
// Generate a printable version
|
||||
var w = window.open('', '_blank');
|
||||
var filtered = getFiltered().filter(function(n) { return !n.has_probe; });
|
||||
var byCountry = {};
|
||||
filtered.forEach(function(n) {
|
||||
var cc = n.country || 'XX';
|
||||
if (!byCountry[cc]) byCountry[cc] = { name: n.country_name || cc, networks: [] };
|
||||
byCountry[cc].networks.push(n);
|
||||
});
|
||||
|
||||
var html = '<!DOCTYPE html><html><head><title>Atlas Coverage Report — Lia\'s Paradise</title>';
|
||||
html += '<style>body{font-family:Arial,sans-serif;margin:2rem;color:#1a1a2e}h1{color:#5b21b6}h2{color:#7c3aed;margin-top:1.5rem;border-bottom:2px solid #e2e8f0;padding-bottom:.3rem}table{width:100%;border-collapse:collapse;margin:.5rem 0 1rem}th,td{text-align:left;padding:.4rem .6rem;border-bottom:1px solid #e2e8f0;font-size:.85rem}th{background:#f8fafc;font-weight:600}.no-probe{color:#dc2626;font-weight:600}.meta{color:#64748b;font-size:.8rem;margin-bottom:2rem}</style></head><body>';
|
||||
html += '<h1>RIPE Atlas Coverage Report</h1>';
|
||||
html += '<div class="meta">Generated by Lia\'s Paradise (peercortex.org/lia) on ' + new Date().toISOString().split('T')[0] + '<br>';
|
||||
html += 'Filter: ' + (currentRIR === 'all' ? 'All RIRs' : currentRIR.toUpperCase()) + ' | Networks without Atlas Probe: ' + filtered.length + '</div>';
|
||||
|
||||
Object.keys(byCountry).sort(function(a,b) { return byCountry[b].networks.length - byCountry[a].networks.length; }).forEach(function(cc) {
|
||||
var c = byCountry[cc];
|
||||
html += '<h2>' + c.name + ' (' + cc + ') — ' + c.networks.length + ' networks</h2>';
|
||||
html += '<table><thead><tr><th>ASN</th><th>Name</th><th>Type</th><th>Status</th></tr></thead><tbody>';
|
||||
c.networks.sort(function(a,b) { return a.asn - b.asn; }).forEach(function(n) {
|
||||
html += '<tr><td>AS' + n.asn + '</td><td>' + (n.name || '') + '</td><td>' + (n.info_type || '-') + '</td><td class="no-probe">No Probe</td></tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
});
|
||||
|
||||
html += '<div class="meta" style="margin-top:3rem">Data from RIPE Atlas & PeeringDB | peercortex.org</div></body></html>';
|
||||
w.document.write(html);
|
||||
w.document.close();
|
||||
w.print();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// File Upload: Parse ASNs/company names from uploaded files
|
||||
// ============================================================
|
||||
function handleFileUpload(input) {
|
||||
var file = input.files[0];
|
||||
if (!file) return;
|
||||
var status = $('fileStatus');
|
||||
status.textContent = 'Processing ' + file.name + '...';
|
||||
status.style.color = 'var(--cyan)';
|
||||
|
||||
var ext = file.name.split('.').pop().toLowerCase();
|
||||
|
||||
if (ext === 'csv' || ext === 'txt') {
|
||||
file.text().then(function(text) { processFileText(text, file.name); });
|
||||
} else if (ext === 'pdf' || ext === 'doc' || ext === 'docx' || ext === 'xls' || ext === 'xlsx') {
|
||||
// For binary formats, upload to server for parsing
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
var base64 = e.target.result.split(',')[1];
|
||||
fetch('/api/lia/parse-file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ filename: file.name, data: base64 })
|
||||
}).then(function(r) { return r.json(); }).then(function(d) {
|
||||
if (d.error) {
|
||||
status.textContent = 'Error: ' + d.error;
|
||||
status.style.color = 'var(--red)';
|
||||
return;
|
||||
}
|
||||
processFileText(d.text || '', file.name);
|
||||
}).catch(function(err) {
|
||||
// Fallback: try reading as text
|
||||
file.text().then(function(text) { processFileText(text, file.name); }).catch(function() {
|
||||
status.textContent = 'Cannot parse ' + ext.toUpperCase() + ' files client-side. Use CSV or TXT.';
|
||||
status.style.color = 'var(--red)';
|
||||
});
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
status.textContent = 'Unsupported file type: ' + ext;
|
||||
status.style.color = 'var(--red)';
|
||||
}
|
||||
}
|
||||
|
||||
function processFileText(text, filename) {
|
||||
var status = $('fileStatus');
|
||||
|
||||
// Extract ASNs (AS12345 or just numbers that look like ASNs)
|
||||
var asnMatches = text.match(/\bAS?(\d{3,7})\b/gi) || [];
|
||||
var asns = new Set();
|
||||
asnMatches.forEach(function(m) {
|
||||
var num = parseInt(m.replace(/^AS/i, ''));
|
||||
if (num >= 100 && num <= 9999999) asns.add(num);
|
||||
});
|
||||
|
||||
// Also try to match company names against our loaded data
|
||||
var nameMatches = [];
|
||||
if (allData.length > 0) {
|
||||
var lines = text.split(/[\n\r,;]+/).map(function(l) { return l.trim().toLowerCase(); }).filter(function(l) { return l.length > 2; });
|
||||
lines.forEach(function(line) {
|
||||
allData.forEach(function(n) {
|
||||
if (n.name && n.name.toLowerCase().indexOf(line) >= 0 && line.length > 4) {
|
||||
asns.add(n.asn);
|
||||
}
|
||||
if (line.indexOf(n.name ? n.name.toLowerCase() : '###') >= 0 && n.name && n.name.length > 4) {
|
||||
asns.add(n.asn);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (asns.size === 0) {
|
||||
status.textContent = 'No ASNs or matching networks found in ' + filename;
|
||||
status.style.color = 'var(--orange)';
|
||||
return;
|
||||
}
|
||||
|
||||
// Match against our data
|
||||
var results = [];
|
||||
var asnArr = Array.from(asns);
|
||||
var dataMap = {};
|
||||
allData.forEach(function(n) { dataMap[n.asn] = n; });
|
||||
|
||||
asnArr.forEach(function(asn) {
|
||||
var n = dataMap[asn];
|
||||
results.push({
|
||||
asn: asn,
|
||||
name: n ? n.name : '(Unknown)',
|
||||
country: n ? n.country : '',
|
||||
info_type: n ? n.info_type : '',
|
||||
has_probe: n ? n.has_probe : false,
|
||||
in_peeringdb: !!n,
|
||||
});
|
||||
});
|
||||
|
||||
results.sort(function(a, b) { return (a.has_probe ? 1 : 0) - (b.has_probe ? 1 : 0); });
|
||||
|
||||
var withProbe = results.filter(function(r) { return r.has_probe; }).length;
|
||||
var noProbe = results.length - withProbe;
|
||||
|
||||
status.innerHTML = '<span style="color:var(--green)">' + withProbe + ' with probe</span> · <span style="color:var(--red)">' + noProbe + ' without probe</span> · ' + results.length + ' total from ' + escHtml(filename);
|
||||
|
||||
var h = '<div style="display:flex;gap:.5rem;margin-bottom:.75rem;flex-wrap:wrap">';
|
||||
h += '<button class="export-btn" onclick="exportFileResults()" style="font-size:.7rem;padding:.3rem .8rem">Export Results as PDF</button>';
|
||||
h += '</div>';
|
||||
h += '<div class="asn-grid">';
|
||||
results.forEach(function(r) {
|
||||
h += '<div class="asn-row" onclick="window.open(\'/?asn=' + r.asn + '\',\'_blank\')">';
|
||||
h += '<span class="asn-num">AS' + r.asn + '</span>';
|
||||
h += '<span class="asn-name">' + escHtml(r.name) + '</span>';
|
||||
if (r.country) h += '<span style="font-size:.65rem;color:var(--dim)">' + flag(r.country) + ' ' + r.country + '</span>';
|
||||
if (r.info_type) h += '<span class="asn-type">' + escHtml(r.info_type) + '</span>';
|
||||
h += r.has_probe ? '<span class="has-probe">\u2714 Probe</span>' : '<span class="no-probe">\u2718 No Probe</span>';
|
||||
if (!r.in_peeringdb) h += '<span style="font-size:.6rem;color:var(--dim)">(not in PeeringDB)</span>';
|
||||
h += '</div>';
|
||||
});
|
||||
h += '</div>';
|
||||
|
||||
$('fileResults').innerHTML = h;
|
||||
$('fileResults').classList.remove('hidden');
|
||||
|
||||
// Store for export
|
||||
window._fileResults = results;
|
||||
window._fileName = filename;
|
||||
}
|
||||
|
||||
function exportFileResults() {
|
||||
if (!window._fileResults) return;
|
||||
var results = window._fileResults;
|
||||
var w = window.open('', '_blank');
|
||||
var html = '<!DOCTYPE html><html><head><title>Atlas Probe Check — ' + escHtml(window._fileName) + '</title>';
|
||||
html += '<style>body{font-family:Arial,sans-serif;margin:2rem;color:#1a1a2e}h1{color:#5b21b6;font-size:1.3rem}table{width:100%;border-collapse:collapse;margin:1rem 0}th,td{text-align:left;padding:.4rem .6rem;border-bottom:1px solid #e2e8f0;font-size:.85rem}th{background:#f8fafc;font-weight:600}.yes{color:#16a34a;font-weight:700}.no{color:#dc2626;font-weight:700}.meta{color:#64748b;font-size:.8rem}</style></head><body>';
|
||||
html += '<h1>Atlas Probe Coverage Check</h1>';
|
||||
html += '<div class="meta">Source: ' + escHtml(window._fileName) + ' | Generated: ' + new Date().toISOString().split('T')[0] + ' | peercortex.org/lia</div>';
|
||||
html += '<table><thead><tr><th>ASN</th><th>Name</th><th>Country</th><th>Type</th><th>Atlas Probe</th></tr></thead><tbody>';
|
||||
results.forEach(function(r) {
|
||||
html += '<tr><td>AS' + r.asn + '</td><td>' + escHtml(r.name) + '</td><td>' + (r.country || '-') + '</td><td>' + (r.info_type || '-') + '</td>';
|
||||
html += '<td class="' + (r.has_probe ? 'yes' : 'no') + '">' + (r.has_probe ? 'YES' : 'NO') + '</td></tr>';
|
||||
});
|
||||
html += '</tbody></table></body></html>';
|
||||
w.document.write(html);
|
||||
w.document.close();
|
||||
w.print();
|
||||
}
|
||||
|
||||
// Boot
|
||||
loadData();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
420
public/public/shell.html
Normal file
420
public/public/shell.html
Normal file
@ -0,0 +1,420 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PeerCortex Shell — Feedback Admin</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body{height:100%;background:#0a0a0a;color:#00ff41;font-family:'IBM Plex Mono','Courier New',monospace;font-size:.82rem;line-height:1.7;overflow:hidden}
|
||||
|
||||
#shell{display:flex;flex-direction:column;height:100vh;max-width:900px;margin:0 auto;padding:0 1rem}
|
||||
|
||||
/* Title bar */
|
||||
#titlebar{display:flex;align-items:center;gap:.5rem;padding:.5rem 0;border-bottom:1px solid rgba(0,255,65,.12);flex-shrink:0;margin-bottom:.25rem}
|
||||
.tb-dot{width:11px;height:11px;border-radius:50%;display:inline-block;flex-shrink:0}
|
||||
#tb-title{flex:1;text-align:center;color:rgba(0,255,65,.45);font-size:.68rem;letter-spacing:.1em}
|
||||
|
||||
/* Output */
|
||||
#output{flex:1;overflow-y:auto;padding:.5rem 0;word-break:break-word}
|
||||
#output div{padding:.05rem 0}
|
||||
|
||||
/* Input bar */
|
||||
#inputbar{display:flex;align-items:center;gap:.5rem;padding:.5rem 0;border-top:1px solid rgba(0,255,65,.12);flex-shrink:0}
|
||||
#prompt{color:rgba(0,255,65,.55);white-space:nowrap;flex-shrink:0}
|
||||
#cmd{flex:1;background:transparent;border:none;outline:none;color:#00ff41;font-family:'IBM Plex Mono','Courier New',monospace;font-size:.82rem;caret-color:#00ff41}
|
||||
|
||||
/* Scrollbar */
|
||||
#output::-webkit-scrollbar{width:4px}
|
||||
#output::-webkit-scrollbar-track{background:transparent}
|
||||
#output::-webkit-scrollbar-thumb{background:rgba(0,255,65,.2);border-radius:2px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="shell">
|
||||
<div id="titlebar">
|
||||
<span class="tb-dot" style="background:#ff5f56"></span>
|
||||
<span class="tb-dot" style="background:#ffbd2e"></span>
|
||||
<span class="tb-dot" style="background:#27c93f"></span>
|
||||
<span id="tb-title">shell.peercortex.org — admin terminal</span>
|
||||
</div>
|
||||
<div id="output"></div>
|
||||
<div id="inputbar">
|
||||
<span id="prompt">shell:~$</span>
|
||||
<input id="cmd" type="text" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var token = null;
|
||||
var allEntries = [];
|
||||
|
||||
var G = 'color:#27c93f';
|
||||
var DIM = 'color:rgba(0,255,65,.35)';
|
||||
var MUT = 'color:rgba(0,255,65,.45)';
|
||||
var Y = 'color:#ffbd2e';
|
||||
var R = 'color:rgba(255,100,100,.8)';
|
||||
var W = 'color:#fff';
|
||||
|
||||
// Safe DOM output — no innerHTML on user data
|
||||
function print(template, fallbackColor) {
|
||||
var out = document.getElementById('output');
|
||||
var line = document.createElement('div');
|
||||
if (fallbackColor) line.style.color = fallbackColor;
|
||||
var parts = template.split(/(<span[^>]*>[^<]*<\/span>)/g);
|
||||
parts.forEach(function(part) {
|
||||
var m = part.match(/^<span([^>]*)>(.*?)<\/span>$/);
|
||||
if (m) {
|
||||
var sp = document.createElement('span');
|
||||
var sm = m[1].match(/style="([^"]*)"/);
|
||||
if (sm) sp.style.cssText = sm[1];
|
||||
sp.textContent = m[2];
|
||||
line.appendChild(sp);
|
||||
} else if (part) {
|
||||
line.appendChild(document.createTextNode(part));
|
||||
}
|
||||
});
|
||||
out.appendChild(line);
|
||||
out.scrollTop = out.scrollHeight;
|
||||
}
|
||||
|
||||
function printText(text, color) {
|
||||
var out = document.getElementById('output');
|
||||
var line = document.createElement('div');
|
||||
line.textContent = text;
|
||||
if (color) line.style.color = color;
|
||||
out.appendChild(line);
|
||||
out.scrollTop = out.scrollHeight;
|
||||
}
|
||||
|
||||
function blank() { print(''); }
|
||||
|
||||
function clear() { document.getElementById('output').textContent = ''; }
|
||||
|
||||
function setPrompt(p) { document.getElementById('prompt').textContent = p; }
|
||||
|
||||
// Boot sequence
|
||||
function boot() {
|
||||
clear();
|
||||
token = null;
|
||||
allEntries = [];
|
||||
setPrompt('shell:~$');
|
||||
var lines = [
|
||||
'<span style="' + DIM + '">══════════════════════════════════════════════════════════</span>',
|
||||
'',
|
||||
' <span style="' + G + ';font-weight:600">PeerCortex — Feedback Administration Shell</span>',
|
||||
' <span style="' + MUT + '">Unauthorized access prohibited. All sessions logged.</span>',
|
||||
'',
|
||||
'<span style="' + DIM + '">══════════════════════════════════════════════════════════</span>',
|
||||
'',
|
||||
'Type <span style="' + G + '">login</span> to authenticate.',
|
||||
''
|
||||
];
|
||||
var d = 0;
|
||||
lines.forEach(function(l){ setTimeout(function(){ print(l); }, d); d += 40; });
|
||||
}
|
||||
|
||||
// Command dispatch
|
||||
function dispatch(raw) {
|
||||
var val = raw.trim();
|
||||
var low = val.toLowerCase();
|
||||
var args = val.split(/\s+/);
|
||||
var cmd = args[0].toLowerCase();
|
||||
|
||||
// Echo input safely
|
||||
(function(){
|
||||
var out = document.getElementById('output');
|
||||
var d = document.createElement('div');
|
||||
var sp = document.createElement('span');
|
||||
sp.style.color = 'rgba(0,255,65,.35)';
|
||||
sp.textContent = document.getElementById('prompt').textContent;
|
||||
d.appendChild(sp);
|
||||
d.appendChild(document.createTextNode(' ' + val));
|
||||
out.appendChild(d);
|
||||
out.scrollTop = out.scrollHeight;
|
||||
})();
|
||||
|
||||
if (!token) {
|
||||
// Unauthenticated mode
|
||||
if (cmd === 'login') {
|
||||
doLogin();
|
||||
} else if (cmd === 'help') {
|
||||
showHelp(false);
|
||||
} else if (cmd === 'clear') {
|
||||
clear();
|
||||
} else if (val === '') {
|
||||
// noop
|
||||
} else {
|
||||
printText('Permission denied. Type login first.', 'rgba(255,100,100,.75)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Authenticated mode
|
||||
if (cmd === 'list') {
|
||||
var cat = args[1] || null;
|
||||
doList(cat);
|
||||
} else if (cmd === 'show') {
|
||||
var idx = parseInt(args[1], 10);
|
||||
doShow(idx);
|
||||
} else if (cmd === 'stats') {
|
||||
doStats();
|
||||
} else if (cmd === 'export') {
|
||||
doExport();
|
||||
} else if (cmd === 'refresh') {
|
||||
doFetch(function(){ print('<span style="' + G + '">✓ Data refreshed. ' + allEntries.length + ' entries loaded.</span>'); });
|
||||
} else if (cmd === 'clear') {
|
||||
clear();
|
||||
} else if (cmd === 'logout') {
|
||||
token = null; allEntries = [];
|
||||
setPrompt('shell:~$');
|
||||
blank();
|
||||
printText('Session terminated.', MUT.slice(6));
|
||||
blank();
|
||||
printText('Type login to re-authenticate.');
|
||||
} else if (cmd === 'help') {
|
||||
showHelp(true);
|
||||
} else if (val === '') {
|
||||
// noop
|
||||
} else {
|
||||
printText('shell: ' + val + ': command not found', 'rgba(255,100,100,.75)');
|
||||
}
|
||||
}
|
||||
|
||||
// Login flow
|
||||
var loginState = 0;
|
||||
function doLogin() {
|
||||
loginState = 1;
|
||||
setPrompt('token:');
|
||||
blank();
|
||||
printText('Enter FEEDBACK_TOKEN:');
|
||||
document.getElementById('cmd').type = 'password';
|
||||
}
|
||||
|
||||
function handleLogin(val) {
|
||||
document.getElementById('cmd').type = 'text';
|
||||
loginState = 0;
|
||||
setPrompt('shell:~$');
|
||||
blank();
|
||||
// Verify token
|
||||
fetch('/api/feedback?token=' + encodeURIComponent(val))
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d){
|
||||
if (d.ok) {
|
||||
token = val;
|
||||
allEntries = d.entries || [];
|
||||
setPrompt('root@peercortex:~#');
|
||||
print('<span style="' + G + '">✓ Authenticated. ' + allEntries.length + ' feedback entries loaded.</span>');
|
||||
blank();
|
||||
showHelp(true);
|
||||
} else {
|
||||
printText('✗ Authentication failed. Wrong token.', 'rgba(255,100,100,.8)');
|
||||
blank();
|
||||
}
|
||||
}).catch(function(){
|
||||
printText('Network error during authentication.', 'rgba(255,100,100,.8)');
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch fresh data from server
|
||||
function doFetch(cb) {
|
||||
fetch('/api/feedback?token=' + encodeURIComponent(token))
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d){
|
||||
if (d.ok) { allEntries = d.entries || []; if (cb) cb(); }
|
||||
else { printText('Fetch error: ' + (d.error || 'unknown'), 'rgba(255,100,100,.8)'); }
|
||||
}).catch(function(){ printText('Network error.', 'rgba(255,100,100,.8)'); });
|
||||
}
|
||||
|
||||
// list [category]
|
||||
function doList(filterCat) {
|
||||
var data = allEntries;
|
||||
if (filterCat) {
|
||||
var lf = filterCat.toLowerCase();
|
||||
data = data.filter(function(e){ return e.category && e.category.toLowerCase().indexOf(lf) >= 0; });
|
||||
}
|
||||
blank();
|
||||
if (data.length === 0) {
|
||||
printText('No entries' + (filterCat ? ' matching "' + filterCat + '"' : '') + '.', MUT.slice(6));
|
||||
blank();
|
||||
return;
|
||||
}
|
||||
print('<span style="' + DIM + '">── # DATE CATEGORY NAME ASN ──</span>');
|
||||
data.forEach(function(e, i) {
|
||||
var realIdx = allEntries.indexOf(e);
|
||||
var date = e.timestamp ? e.timestamp.slice(0,10) : '?';
|
||||
var cat = (e.category || 'General').slice(0,18).padEnd(18);
|
||||
var name = (e.name || 'Anonymous').slice(0,16).padEnd(16);
|
||||
var asn = e.asn ? ('AS' + e.asn) : '—';
|
||||
var num = String(realIdx + 1).padStart(3);
|
||||
(function(entry, n){
|
||||
var out = document.getElementById('output');
|
||||
var line = document.createElement('div');
|
||||
var snr = document.createElement('span');
|
||||
snr.style.color = 'rgba(0,255,65,.4)';
|
||||
snr.textContent = num + ' ';
|
||||
var sdat = document.createElement('span');
|
||||
sdat.style.color = '#ffbd2e';
|
||||
sdat.textContent = date + ' ';
|
||||
var scat = document.createElement('span');
|
||||
scat.style.color = '#00ff41';
|
||||
scat.textContent = cat + ' ';
|
||||
var snm = document.createElement('span');
|
||||
snm.style.color = 'rgba(0,255,65,.7)';
|
||||
snm.textContent = name + ' ';
|
||||
var sasn = document.createElement('span');
|
||||
sasn.style.color = 'rgba(0,255,65,.45)';
|
||||
sasn.textContent = asn;
|
||||
line.appendChild(snr); line.appendChild(sdat); line.appendChild(scat);
|
||||
line.appendChild(snm); line.appendChild(sasn);
|
||||
out.appendChild(line);
|
||||
out.scrollTop = out.scrollHeight;
|
||||
})(e, realIdx);
|
||||
});
|
||||
print('<span style="' + DIM + '">──────────────────────────────────────────────────────────────</span>');
|
||||
var out2 = document.getElementById('output');
|
||||
var tot = document.createElement('div');
|
||||
tot.style.color = 'rgba(0,255,65,.45)';
|
||||
tot.textContent = data.length + ' entr' + (data.length === 1 ? 'y' : 'ies') + (filterCat ? ' (filtered)' : '') + ' — type show <n> for full message';
|
||||
out2.appendChild(tot);
|
||||
out2.scrollTop = out2.scrollHeight;
|
||||
blank();
|
||||
}
|
||||
|
||||
// show <n>
|
||||
function doShow(n) {
|
||||
if (isNaN(n) || n < 1 || n > allEntries.length) {
|
||||
printText('Usage: show <number> (1–' + allEntries.length + ')', 'rgba(255,189,46,.8)');
|
||||
return;
|
||||
}
|
||||
var e = allEntries[n - 1];
|
||||
blank();
|
||||
print('<span style="' + DIM + '">────────────────────────────────────────────────────────────</span>');
|
||||
(function(){
|
||||
var fields = [
|
||||
['ID', e.id || '?'],
|
||||
['Timestamp', e.timestamp || '?'],
|
||||
['Category', e.category || 'General'],
|
||||
['Name', e.name || 'Anonymous'],
|
||||
['ASN', e.asn ? 'AS' + e.asn : '—'],
|
||||
['IP', e.ip || '—'],
|
||||
];
|
||||
fields.forEach(function(f){
|
||||
var out = document.getElementById('output');
|
||||
var d = document.createElement('div');
|
||||
var lbl = document.createElement('span');
|
||||
lbl.style.color = 'rgba(0,255,65,.45)';
|
||||
lbl.textContent = f[0].padEnd(12) + ' ';
|
||||
var val = document.createElement('span');
|
||||
val.style.color = '#00ff41';
|
||||
val.textContent = f[1];
|
||||
d.appendChild(lbl); d.appendChild(val);
|
||||
out.appendChild(d);
|
||||
out.scrollTop = out.scrollHeight;
|
||||
});
|
||||
})();
|
||||
blank();
|
||||
printText('Message:', 'rgba(0,255,65,.45)');
|
||||
printText(e.message || '(empty)', '#fff');
|
||||
blank();
|
||||
print('<span style="' + DIM + '">────────────────────────────────────────────────────────────</span>');
|
||||
blank();
|
||||
}
|
||||
|
||||
// stats
|
||||
function doStats() {
|
||||
if (allEntries.length === 0) { printText('No feedback entries yet.', MUT.slice(6)); return; }
|
||||
var cats = {};
|
||||
var asns = {};
|
||||
allEntries.forEach(function(e){
|
||||
var c = e.category || 'General';
|
||||
cats[c] = (cats[c] || 0) + 1;
|
||||
if (e.asn) { asns[e.asn] = (asns[e.asn] || 0) + 1; }
|
||||
});
|
||||
blank();
|
||||
print('<span style="' + G + ';font-weight:600">─── Feedback Statistics ───────────────────</span>');
|
||||
var out = document.getElementById('output');
|
||||
var tot = document.createElement('div');
|
||||
tot.style.color = '#ffbd2e';
|
||||
tot.textContent = 'Total entries: ' + allEntries.length;
|
||||
out.appendChild(tot);
|
||||
blank();
|
||||
printText('By category:', 'rgba(0,255,65,.45)');
|
||||
Object.keys(cats).sort(function(a,b){ return cats[b]-cats[a]; }).forEach(function(c){
|
||||
var d2 = document.createElement('div');
|
||||
var bar = '█'.repeat(Math.round(cats[c] / allEntries.length * 20));
|
||||
var sp1 = document.createElement('span');
|
||||
sp1.style.color = 'rgba(0,255,65,.4)';
|
||||
sp1.textContent = ' ' + c.padEnd(20);
|
||||
var sp2 = document.createElement('span');
|
||||
sp2.style.color = '#27c93f';
|
||||
sp2.textContent = bar + ' ' + cats[c];
|
||||
d2.appendChild(sp1); d2.appendChild(sp2);
|
||||
out.appendChild(d2);
|
||||
});
|
||||
var topAsn = Object.keys(asns);
|
||||
if (topAsn.length > 0) {
|
||||
blank();
|
||||
printText('Top ASNs reported:', 'rgba(0,255,65,.45)');
|
||||
topAsn.sort(function(a,b){ return asns[b]-asns[a]; }).slice(0,5).forEach(function(a){
|
||||
var d3 = document.createElement('div');
|
||||
d3.style.color = 'rgba(0,255,65,.7)';
|
||||
d3.textContent = ' AS' + a + ' — ' + asns[a] + ' report' + (asns[a]===1?'':'s');
|
||||
out.appendChild(d3);
|
||||
});
|
||||
}
|
||||
out.scrollTop = out.scrollHeight;
|
||||
blank();
|
||||
}
|
||||
|
||||
// export — trigger JSON file download
|
||||
function doExport() {
|
||||
var blob = new Blob([JSON.stringify(allEntries, null, 2)], {type:'application/json'});
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'peercortex-feedback-' + new Date().toISOString().slice(0,10) + '.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
print('<span style="' + G + '">✓ Export downloaded: ' + allEntries.length + ' entries.</span>');
|
||||
blank();
|
||||
}
|
||||
|
||||
// help
|
||||
function showHelp(authed) {
|
||||
blank();
|
||||
print('<span style="' + G + ';font-weight:600">Available commands:</span>');
|
||||
if (!authed) {
|
||||
print(' <span style="' + G + '">login</span> — authenticate with FEEDBACK_TOKEN');
|
||||
print(' <span style="' + G + '">clear</span> — clear screen');
|
||||
} else {
|
||||
print(' <span style="' + G + '">list</span> — show all feedback entries');
|
||||
print(' <span style="' + G + '">list [category]</span> — filter by category (bug, feature, design, general)');
|
||||
print(' <span style="' + G + '">show [n]</span> — show full message for entry #n');
|
||||
print(' <span style="' + G + '">stats</span> — category + ASN statistics');
|
||||
print(' <span style="' + G + '">export</span> — download all entries as JSON');
|
||||
print(' <span style="' + G + '">refresh</span> — reload entries from server');
|
||||
print(' <span style="' + G + '">clear</span> — clear screen');
|
||||
print(' <span style="' + G + '">logout</span> — end session');
|
||||
}
|
||||
blank();
|
||||
}
|
||||
|
||||
// Input handler
|
||||
document.getElementById('cmd').addEventListener('keydown', function(e){
|
||||
if (e.key !== 'Enter') return;
|
||||
var val = this.value;
|
||||
this.value = '';
|
||||
if (loginState === 1) {
|
||||
handleLogin(val.trim());
|
||||
} else {
|
||||
dispatch(val);
|
||||
}
|
||||
});
|
||||
|
||||
// Start
|
||||
boot();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
11
scripts/refresh-peeringdb.sh
Executable file
11
scripts/refresh-peeringdb.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
# Refresh PeeringDB SQLite mirror daily
|
||||
|
||||
LOG=/var/log/peercortex/peeringdb-refresh.log
|
||||
mkdir -p /var/log/peercortex
|
||||
exec >> $LOG 2>&1
|
||||
|
||||
echo "[$(date +%Y-%m-%dT%H:%M:%S)] Starting PeeringDB sync..."
|
||||
cd /opt/peeringdb-data
|
||||
peeringdb sync -q
|
||||
echo "[$(date +%Y-%m-%dT%H:%M:%S)] Sync complete. File: $(du -sh peeringdb.sqlite3 | cut -f1)"
|
||||
29
scripts/tunnel-cleanup.sh
Executable file
29
scripts/tunnel-cleanup.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# Auto-heal Cloudflare tunnel: clean stale connectors from foreign IPs
|
||||
TUNNEL=3262c64b-51d5-479f-ad26-a9925b705bd3
|
||||
LOG=/var/log/peercortex/tunnel-cleanup.log
|
||||
|
||||
mkdir -p /var/log/peercortex
|
||||
|
||||
# Restart cloudflared if not running
|
||||
if ! systemctl is-active --quiet cloudflared; then
|
||||
echo "[$(date +%Y-%m-%dT%H:%M:%S)] cloudflared not running — starting" >> "$LOG"
|
||||
systemctl start cloudflared
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
MY_IP=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
|
||||
[ -z "$MY_IP" ] && exit 0
|
||||
|
||||
# Connector rows start with a UUID (36 chars: 8-4-4-4-12)
|
||||
# Column 5 in connector rows = ORIGIN IP
|
||||
STALE=$(cloudflared tunnel info "$TUNNEL" 2>/dev/null | \
|
||||
grep -E '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | \
|
||||
awk -v myip="$MY_IP" '$5 != myip {print $5}')
|
||||
|
||||
if [ -n "$STALE" ]; then
|
||||
echo "[$(date +%Y-%m-%dT%H:%M:%S)] Stale connectors from $STALE — cleaning" >> "$LOG"
|
||||
cloudflared tunnel cleanup "$TUNNEL" >> "$LOG" 2>&1
|
||||
systemctl restart cloudflared
|
||||
echo "[$(date +%Y-%m-%dT%H:%M:%S)] Done" >> "$LOG"
|
||||
fi
|
||||
1
webhook-subs.json
Normal file
1
webhook-subs.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
Loading…
x
Reference in New Issue
Block a user