Skip to content

Commit ea6dfb2

Browse files
committed
otp scan PRs for vulnerabilities
- perform vulnerability analysis on a pull requests basis and on a scheduled basis. - adds the option `osv-scan` to `otp-compliance.es` to submit requests to OSV API. - adds hard-coded VEX filter to ignore CVEs for which OTP is not vulnerable, using the last three releases that we maintain. - creation of reusable Github workflow to allow direct calls (workflow_call) and gh triggering events (workflow_dispatch).
1 parent a3f4728 commit ea6dfb2

File tree

13 files changed

+427
-37
lines changed

13 files changed

+427
-37
lines changed

.github/scripts/otp-compliance.es

Lines changed: 289 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,19 @@ cli() ->
220220
> .github/scripts/otp-compliance.es sbom vendor --sbom-file otp.spdx.json
221221
""",
222222
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}
224236
}},
225237
"explore" =>
226238
#{ help => """
@@ -320,6 +332,17 @@ sbom_option() ->
320332
default => "bom.spdx.json",
321333
long => "-sbom-file"}.
322334

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+
323346
ntia_checker() ->
324347
#{name => ntia_checker,
325348
type => boolean,
@@ -1297,6 +1320,270 @@ generate_vendor_purl(Package) ->
12971320
[create_externalRef_purl(Description, <<Purl/binary, "@", Vsn/binary>>)]
12981321
end.
12991322

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+
13001587
cleanup_path(<<"./", Path/binary>>) when is_binary(Path) -> Path;
13011588
cleanup_path(Path) when is_binary(Path) -> Path.
13021589

@@ -1672,7 +1959,7 @@ root_vendor_packages() ->
16721959
minimum_vendor_packages() ->
16731960
%% self-contained
16741961
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"].
16761963

16771964
test_copyright_not_empty(#{~"packages" := Packages}) ->
16781965
true = lists:all(fun (#{~"copyrightText" := Copyright}) -> Copyright =/= ~"" end, Packages),

.github/workflows/main.yaml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,15 @@ jobs:
426426
docker run otp "erl ${OPTION} -noshell -s init stop"
427427
done
428428
429+
# this is a call to a workflow_call
430+
pr-vendor-vulnerability-analysis:
431+
name: Vendor Vulnerability Scanning
432+
uses: ./.github/workflows/reusable-vendor-vulnerability-scanner.yml
433+
with:
434+
sarif: false
435+
version: ${{ github.event_name == 'pull_request' && github.base_ref || github.ref_name }}
436+
# equivalent of ${{ env.BASE_BRANCH }} but reusable-workflows do not allow to pass env.
437+
429438
build:
430439
name: Build Erlang/OTP
431440
runs-on: ubuntu-latest
@@ -848,18 +857,17 @@ jobs:
848857
fail-on: ${{ github.ref_type == 'tag' && '' || 'violations,issues' }}
849858
sw-version: ${{ env.OTP_SBOM_VERSION }}
850859

851-
vendor-analysis:
852-
name: Vendor Dependency Analysis
860+
vendor-dependency-upload:
861+
name: Vendor Dependency Upload
853862
runs-on: ubuntu-latest
854-
if: github.event_name == 'push'
855863
needs:
856864
- sbom
857865
- pack
866+
if: github.repository == 'erlang/otp'
858867
## Needed to use Github Dependency API
859868
permissions:
860869
contents: write
861870
id-token: write
862-
863871
steps:
864872
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/[email protected]
865873
- uses: ./.github/actions/build-base-image
@@ -878,7 +886,9 @@ jobs:
878886
--sbom-file /github/bom.spdx.json"
879887
880888
# allows Dependabot to give us alert of the vendor libraries that use semantic versioning
889+
# it also allows dependencies to be looked up from github dependencies
881890
- name: Upload SBOM to Github Dependency API
891+
if: github.event_name == 'push'
882892
uses: advanced-security/spdx-dependency-submission-action@5530bab9ee4bbe66420ce8280624036c77f89746 # ratchet:advanced-security/[email protected]
883893

884894
## If this is an "OTP-*" tag that has been pushed we do some release work

0 commit comments

Comments
 (0)