@@ -220,7 +220,19 @@ cli() ->
220
220
> .github/scripts/otp-compliance.es sbom vendor --sbom-file otp.spdx.json
221
221
""" ,
222
222
arguments => [ sbom_option ()],
223
- handler => fun sbom_vendor /1 }
223
+ handler => fun sbom_vendor /1 },
224
+
225
+ " osv-scan" =>
226
+ #{ help =>
227
+ """
228
+ Performs vulnerability scanning on vendor libraries
229
+
230
+ Example:
231
+
232
+ > .github/scripts/otp-compliance.es sbom osv-scan
233
+ """ ,
234
+ arguments => [ versions_file (), sarif_option () ],
235
+ handler => fun osv_scan /1 }
224
236
}},
225
237
" explore" =>
226
238
#{ help => """
@@ -320,6 +332,17 @@ sbom_option() ->
320
332
default => " bom.spdx.json" ,
321
333
long => " -sbom-file" }.
322
334
335
+ versions_file () ->
336
+ #{name => version ,
337
+ type => binary ,
338
+ long => " -version" }.
339
+
340
+ sarif_option () ->
341
+ #{name => sarif ,
342
+ type => boolean ,
343
+ default => true ,
344
+ long => " -sarif" }.
345
+
323
346
ntia_checker () ->
324
347
#{name => ntia_checker ,
325
348
type => boolean ,
@@ -1297,6 +1320,270 @@ generate_vendor_purl(Package) ->
1297
1320
[create_externalRef_purl (Description , <<Purl /binary , " @" , Vsn /binary >>)]
1298
1321
end .
1299
1322
1323
+ osv_scan (#{version := Version , sarif := Sarif }) ->
1324
+ application :ensure_all_started ([ssl , inets ]),
1325
+ OSVQuery = vendor_by_version (Version ),
1326
+
1327
+ io :format (" [OSV] Information sent~n~s~n " , [json :format (OSVQuery )]),
1328
+
1329
+ OSV = json :encode (OSVQuery ),
1330
+
1331
+ Format = " application/x-www-form-urlencoded" ,
1332
+ URI = " https://api.osv.dev/v1/querybatch" ,
1333
+ Content = {URI , [], Format , OSV },
1334
+ Result = httpc :request (post , Content , [], []),
1335
+ Vulns =
1336
+ case Result of
1337
+ {ok ,{{_ , 200 ,_ }, _Headers , Body }} ->
1338
+ #{~ " results" := OSVResults } = json :decode (erlang :list_to_binary (Body )),
1339
+ Vulnerabilities = lists :filter (fun (#{~ " vulns" := _Ids }) -> true ; (_ ) -> false end , OSVResults ),
1340
+ case Vulnerabilities of
1341
+ [] ->
1342
+ [];
1343
+ _ ->
1344
+ NameVulnerabilities = lists :zip (osv_names (OSVQuery ), OSVResults ),
1345
+ lists :filtermap (fun ({Name , #{~ " vulns" := Ids }}) ->
1346
+ {true , {Name , [Id || #{~ " id" := Id } <- Ids ]}};
1347
+ (_ ) ->
1348
+ false
1349
+ end , NameVulnerabilities )
1350
+ end ;
1351
+ {error , Error } ->
1352
+ {error , [URI , Error ]}
1353
+ end ,
1354
+ Vulns1 = ignore_vex_cves (Vulns ),
1355
+ ok = generate_sarif (Sarif , Vulns1 ),
1356
+ FormattedVulns = format_vulnerabilities (Vulns1 ),
1357
+ report_vulnerabilities (FormattedVulns ).
1358
+
1359
+ generate_sarif (false , _Vulns ) ->
1360
+ io :format (" [SARIF] No sarif file generated~n~n " ),
1361
+ ok ;
1362
+ generate_sarif (true , Vulns ) ->
1363
+ SarifFilename = " results.sarif" ,
1364
+
1365
+ {ok , Cwd } = file :get_cwd (),
1366
+ io :format (" [SARIF] Generating Sarif: ~s~n " , [Cwd ++ " /" ++ SarifFilename ]),
1367
+ io :format (" ok~n~n " ),
1368
+
1369
+ Sarif = json :format (generate_sarif (Vulns )),
1370
+ file :write_file (SarifFilename , Sarif ).
1371
+
1372
+ generate_sarif (Vulns ) ->
1373
+ #{ ~ " version" => ~ " 2.1.0" ,
1374
+ ~ " $schema" => ~ " https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json" ,
1375
+ ~ " runs" =>
1376
+ [ #{
1377
+ ~ " tool" =>
1378
+ #{ ~ " driver" =>
1379
+ #{ ~ " informationUri" => ~ " https://github.com/erlang/otp/scripts/otp-compliance.es" ,
1380
+ ~ " name" => ~ " otp-compliance" ,
1381
+ ~ " rules" =>
1382
+ [ #{ ~ " id" => ~ " CVE-OTP-VENDOR" ,
1383
+ ~ " name" => ~ " CVEInDependency" ,
1384
+ ~ " shortDescription" =>
1385
+ #{ ~ " text" => ~ " CVE found in dependency" },
1386
+ ~ " fullDescription" =>
1387
+ #{
1388
+ ~ " text" => ~ " CVE found in OTP runtime dependency"
1389
+ }
1390
+ }],
1391
+ ~ " version" => ~ " 1.0"
1392
+ }
1393
+ },
1394
+ ~ " results" =>
1395
+ [ #{
1396
+ ~ " ruleId" => ~ " CVE-OTP-VENDOR" ,
1397
+ ~ " ruleIndex" => 0 , % matches rule object that should apply
1398
+ ~ " level" => ~ " warning" ,
1399
+ ~ " message" => #{ ~ " text" => error_to_text ({Dependency , CVE }) },
1400
+ ~ " locations" =>
1401
+ [ #{ ~ " physicalLocation" =>
1402
+ #{ ~ " artifactLocation" =>
1403
+ #{ ~ " uri" => Dependency }}}
1404
+ ]
1405
+ } || {Dependency , CVEs } <- Vulns , CVE <- CVEs ],
1406
+ ~ " artifacts" =>
1407
+ [ #{ ~ " location" => #{ ~ " uri" => Dependency },
1408
+ ~ " length" => - 1
1409
+ } || {Dependency , _ } <- Vulns ]
1410
+ }]
1411
+ }.
1412
+
1413
+ error_to_text ({Dependency , Vuln }) ->
1414
+ <<" Dependency " , Dependency /binary , " has " , Vuln /binary >>.
1415
+
1416
+ % % TODO: fix by reading VEX files from erlang/vex or repo containing VEX files
1417
+ ignore_vex_cves (Vulns ) ->
1418
+ lists :foldl (fun ({~ " github.com/wxWidgets/wxWidgets" , _CVEs }, Acc ) ->
1419
+ % % OTP cannot be vulnerable to wxwidgets because
1420
+ % % we only take documentation.
1421
+ Acc ;
1422
+ ({Name , CVEs }, Acc ) ->
1423
+ case maps :get (Name , non_vulnerable_cves (), not_found ) of
1424
+ not_found ->
1425
+ [{Name , CVEs } | Acc ];
1426
+ NonCVEs ->
1427
+ case CVEs -- NonCVEs of
1428
+ [] ->
1429
+ Acc ;
1430
+ Vs ->
1431
+ [{Name , Vs } | Acc ]
1432
+ end
1433
+ end
1434
+ end , [], Vulns ).
1435
+
1436
+ non_vulnerable_cves () -> #{}.
1437
+ % % #{ ~"github.com/madler/zlib" => [~"CVE-2023-45853"],
1438
+ % % ~"github.com/openssl/openssl" =>
1439
+ % % [~"CVE-2024-12797", ~"CVE-2023-6129", ~"CVE-2023-6237", ~"CVE-2024-0727",
1440
+ % % ~"CVE-2024-13176", ~"CVE-2024-2511", ~"CVE-2024-4603", ~"CVE-2024-4741",
1441
+ % % ~"CVE-2024-5535", ~"CVE-2024-6119", ~"CVE-2024-9143"],
1442
+ % % ~"github.com/PCRE2Project/pcre2" => [~"OSV-2025-300"]}.
1443
+
1444
+
1445
+ format_vulnerabilities ({error , ErrorContext }) ->
1446
+ {error , ErrorContext };
1447
+ format_vulnerabilities (ExistingVulnerabilities ) when is_list (ExistingVulnerabilities ) ->
1448
+ lists :map (fun ({N , Ids }) ->
1449
+ io_lib :format (" - ~s : ~s~n " , [N , lists :join (" ," , Ids )])
1450
+ end , ExistingVulnerabilities ).
1451
+
1452
+ report_vulnerabilities ([]) ->
1453
+ io :format (" [OSV] No vulnerabilities found.~n " );
1454
+ report_vulnerabilities ({error , [URI , Error ]}) ->
1455
+ fail (" [OSV] POST request to ~p errors: ~p " , [URI , Error ]);
1456
+ report_vulnerabilities (FormatVulns ) ->
1457
+ io :format (" [OSV] There are existing vulnerabilities:~n~s " , [FormatVulns ]).
1458
+
1459
+ osv_names (#{~ " queries" := Packages }) ->
1460
+ lists :map (fun osv_names /1 , Packages );
1461
+ osv_names (#{~ " package" := #{~ " name" := Name }}) ->
1462
+ Name .
1463
+
1464
+ generate_osv_query (Packages ) ->
1465
+ #{~ " queries" => lists :foldl (fun generate_osv_query /2 , [], Packages )}.
1466
+ generate_osv_query (#{~ " versionInfo" := Vsn , ~ " ecosystem" := Ecosystem , ~ " name" := Name }, Acc ) ->
1467
+ Package = #{~ " package" => #{~ " name" => Name , ~ " ecosystem" => Ecosystem }, ~ " version" => Vsn },
1468
+ [Package | Acc ];
1469
+ generate_osv_query (#{~ " sha" := SHA , ~ " downloadLocation" := Location }, Acc ) ->
1470
+ case string :prefix (Location , ~ " https://" ) of
1471
+ nomatch ->
1472
+ Acc ;
1473
+ URI ->
1474
+ Package = #{~ " package" => #{~ " name" => URI }, ~ " commit" => SHA },
1475
+ [Package | Acc ]
1476
+ end ;
1477
+ generate_osv_query (_ , Acc ) ->
1478
+ Acc .
1479
+
1480
+ % % when we no longer need to maintain maint-27, we can remove
1481
+ % % this hard-coded commits and versions.
1482
+ vendor_by_version (~ " maint-25" ) ->
1483
+ #{~ " queries" =>
1484
+ [#{~ " commit" => ~ " 21767c654d31d2dccdde4330529775c6c5fd5389" ,
1485
+ ~ " package" => #{~ " name" => ~ " github.com/madler/zlib" }},
1486
+
1487
+ #{~ " commit" => ~ " 23ddf56b00f47d8aa0c82ad225e4b3a92661da7e" ,
1488
+ ~ " package" => #{~ " name" => ~ " github.com/asmjit/asmjit" }},
1489
+
1490
+ #{ ~ " commit" => ~ " e745bad3b1d05b5b19ec652d68abb37865ffa454" ,
1491
+ ~ " package" => #{~ " name" => ~ " github.com/microsoft/STL" }},
1492
+
1493
+ #{~ " commit" => ~ " 844864ac213bdbf1fb57e6f51c653b3d90af0937" ,
1494
+ ~ " package" => #{~ " name" => ~ " github.com/ulfjack/ryu" }},
1495
+
1496
+ #{ ~ " commit" => ~ " 01d5e2318405362b4de5e670c90d9b40a351d053" ,
1497
+ ~ " package" => #{~ " name" => ~ " github.com/openssl/openssl" }},
1498
+
1499
+ #{ % 8.45, not offial but the official sourceforge is not available
1500
+ ~ " commit" => ~ " 3934406b50b8c2a4e2fc7362ed8026224ac90828" ,
1501
+ ~ " package" => #{~ " name" => ~ " github.com/nektro/pcre-8.45" }},
1502
+
1503
+ #{~ " commit" => ~ " dc585039bbd426829e3433002023a93f9bedd0c2" ,
1504
+ ~ " package" => #{~ " name" => ~ " github.com/wxWidgets/wxWidgets" }},
1505
+
1506
+ #{~ " version" => ~ " 2.32" ,
1507
+ ~ " package" => #{~ " ecosystem" => ~ " npm" ,
1508
+ ~ " name" => ~ " tablesorter" }},
1509
+
1510
+ #{~ " version" => ~ " 3.7.1" ,
1511
+ ~ " package" => #{~ " ecosystem" => ~ " npm" ,
1512
+ ~ " name" => ~ " jquery" }}
1513
+ ]};
1514
+ vendor_by_version (~ " maint-26" ) ->
1515
+ #{~ " queries" =>
1516
+ [#{% % v1.2.13
1517
+ ~ " commit" => ~ " 04f42ceca40f73e2978b50e93806c2a18c1281fc" ,
1518
+ ~ " package" => #{~ " name" => ~ " github.com/madler/zlib" }},
1519
+
1520
+ #{~ " commit" => ~ " 915186f6c5c2f5a4638e5cb97ccc23d741521a64" ,
1521
+ ~ " package" => #{~ " name" => ~ " github.com/asmjit/asmjit" }},
1522
+
1523
+ #{~ " commit" => ~ " e745bad3b1d05b5b19ec652d68abb37865ffa454" ,
1524
+ ~ " package" => #{~ " name" => ~ " github.com/microsoft/STL" }},
1525
+
1526
+ #{~ " commit" => ~ " 844864ac213bdbf1fb57e6f51c653b3d90af0937" ,
1527
+ ~ " package" => #{~ " name" => ~ " github.com/ulfjack/ryu" }},
1528
+
1529
+ #{% 3.1.4
1530
+ ~ " commit" => ~ " 01d5e2318405362b4de5e670c90d9b40a351d053" ,
1531
+ ~ " package" => #{~ " name" => ~ " github.com/openssl/openssl" }},
1532
+
1533
+ #{% 8.45, not offial but the official sourceforge is not available
1534
+ ~ " commit" => ~ " 3934406b50b8c2a4e2fc7362ed8026224ac90828" ,
1535
+ ~ " package" => #{~ " name" => ~ " github.com/nektro/pcre-8.45" }},
1536
+
1537
+ #{~ " commit" => ~ " dc585039bbd426829e3433002023a93f9bedd0c2" ,
1538
+ ~ " package" => #{~ " name" => ~ " github.com/wxWidgets/wxWidgets" }},
1539
+
1540
+ #{~ " version" => ~ " 2.32" ,
1541
+ ~ " package" => #{~ " ecosystem" => ~ " npm" ,
1542
+ ~ " name" => ~ " tablesorter" }},
1543
+
1544
+ #{~ " version" => ~ " 3.7.1" ,
1545
+ ~ " package" => #{~ " ecosystem" => ~ " npm" ,
1546
+ ~ " name" => ~ " jquery" }}
1547
+ ]};
1548
+ vendor_by_version (~ " maint-27" ) ->
1549
+ #{~ " queries" =>
1550
+ [#{ % % v1.2.13
1551
+ ~ " commit" => ~ " 04f42ceca40f73e2978b50e93806c2a18c1281fc" ,
1552
+ ~ " package" => #{~ " name" => ~ " github.com/madler/zlib" }},
1553
+
1554
+ #{~ " commit" => ~ " a465fe71ab3d0e224b2b4bd0fac69ae68ab9239d" ,
1555
+ ~ " package" => #{ ~ " name" => ~ " github.com/asmjit/asmjit" }},
1556
+
1557
+ #{~ " commit" => ~ " e745bad3b1d05b5b19ec652d68abb37865ffa454" ,
1558
+ ~ " package" => #{~ " name" => ~ " github.com/microsoft/STL" }},
1559
+
1560
+ #{~ " commit" => ~ " 844864ac213bdbf1fb57e6f51c653b3d90af0937" ,
1561
+ ~ " package" => #{~ " name" => ~ " github.com/ulfjack/ryu" }},
1562
+
1563
+ #{ % 3.1.4
1564
+ ~ " commit" => ~ " 01d5e2318405362b4de5e670c90d9b40a351d053" ,
1565
+ ~ " package" => #{~ " name" => ~ " github.com/openssl/openssl" }},
1566
+
1567
+ #{% 8.45, not offial but the official sourceforge is not available
1568
+ ~ " commit" => ~ " 3934406b50b8c2a4e2fc7362ed8026224ac90828" ,
1569
+ ~ " package" => #{ ~ " name" => ~ " github.com/nektro/pcre-8.45" }},
1570
+
1571
+ #{~ " commit" => ~ " dc585039bbd426829e3433002023a93f9bedd0c2" ,
1572
+ ~ " package" => #{~ " name" => ~ " github.com/wxWidgets/wxWidgets" }},
1573
+
1574
+ #{~ " version" => ~ " 2.32" ,
1575
+ ~ " package" => #{~ " ecosystem" => ~ " npm" ,
1576
+ ~ " name" => ~ " tablesorter" }},
1577
+
1578
+ #{~ " version" => ~ " 3.7.1" ,
1579
+ ~ " package" => #{~ " ecosystem" => ~ " npm" ,
1580
+ ~ " name" => ~ " jquery" }}
1581
+ ]};
1582
+ vendor_by_version (_ ) ->
1583
+ VendorSrcFiles = find_vendor_src_files (" ." ),
1584
+ Packages = generate_vendor_info_package (VendorSrcFiles ),
1585
+ generate_osv_query (Packages ).
1586
+
1300
1587
cleanup_path (<<" ./" , Path /binary >>) when is_binary (Path ) -> Path ;
1301
1588
cleanup_path (Path ) when is_binary (Path ) -> Path .
1302
1589
@@ -1672,7 +1959,7 @@ root_vendor_packages() ->
1672
1959
minimum_vendor_packages () ->
1673
1960
% % self-contained
1674
1961
root_vendor_packages () ++
1675
- [~ " tcl" , ~ " STL" , ~ " json-test-suite" , ~ " openssl" , ~ " Autoconf" , ~ " wx" , ~ " jquery" , ~ " jquery- tablesorter" ].
1962
+ [~ " tcl" , ~ " STL" , ~ " json-test-suite" , ~ " openssl" , ~ " Autoconf" , ~ " wx" , ~ " jquery" , ~ " tablesorter" ].
1676
1963
1677
1964
test_copyright_not_empty (#{~ " packages" := Packages }) ->
1678
1965
true = lists :all (fun (#{~ " copyrightText" := Copyright }) -> Copyright =/= ~ " " end , Packages ),
0 commit comments