-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvulnerability_alerts.exs
executable file
·142 lines (121 loc) · 4.99 KB
/
vulnerability_alerts.exs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
Mix.install([
{:jason, "~> 1.2"},
{:neuron, "~> 5.0.0"},
{:httpoison, "~> 1.8"}
])
defmodule VulnerabilityAlerts do
# second naming is for easier local testing
@github_token System.get_env("INPUT_GITHUB-TOKEN") || System.get_env("INPUT_GITHUB_TOKEN")
@github_repository System.get_env("INPUT_GITHUB-REPOSITORY") || System.get_env("INPUT_GITHUB_REPOSITORY")
@slack_token System.get_env("INPUT_SLACK-TOKEN") || System.get_env("INPUT_SLACK_TOKEN")
@slack_channel System.get_env("INPUT_SLACK-CHANNEL") || System.get_env("INPUT_SLACK_CHANNEL")
def run do
@github_repository
|> build_query()
|> request()
|> handle_response()
|> post_to_slack()
end
defp build_query(full_name) do
[owner, name] = String.split(full_name, "/")
"""
{
repository(owner: "#{owner}", name: "#{name}") {
nameWithOwner
vulnerabilityAlerts(first: 100, states: OPEN) {
nodes {
fixedAt
dismissedAt
state
vulnerableRequirements
securityVulnerability {
advisory {
severity
summary
notificationsPermalink
}
package {
name
ecosystem
}
firstPatchedVersion {
identifier
}
}
}
}
}
}
"""
end
defp request(query) do
Neuron.query(query, %{},
url: "https://api.github.com/graphql",
headers: [Accept: "application/vnd.github.hawkgirl-preview+json", authorization: "Bearer #{@github_token}"]
)
end
defp handle_response({:ok, %Neuron.Response{body: body}}) do
full_name = body["data"]["repository"]["nameWithOwner"]
body["data"]["repository"]["vulnerabilityAlerts"]["nodes"]
|> Enum.reduce([], fn vulnerability_alert, acc ->
va = %{
package: %{name: vulnerability_alert["securityVulnerability"]["package"]["name"], ecosystem: vulnerability_alert["securityVulnerability"]["package"]["ecosystem"]},
vulnerable_requirements: vulnerability_alert["vulnerableRequirements"],
notifications_permalink: vulnerability_alert["securityVulnerability"]["advisory"]["notificationsPermalink"],
summary: vulnerability_alert["securityVulnerability"]["advisory"]["summary"],
severity: vulnerability_alert["securityVulnerability"]["advisory"]["severity"],
first_patched_version: vulnerability_alert["securityVulnerability"]["firstPatchedVersion"]["identifier"],
resolve_issue_url: resolve_issue_url(full_name, vulnerability_alert["securityVulnerability"]["package"]),
dismissed_at: parse_datetime(vulnerability_alert["dismissedAt"]),
fixed_at: parse_datetime(vulnerability_alert["fixedAt"]),
state: vulnerability_alert["state"]
}
[va | acc]
end)
end
defp resolve_issue_url(full_name, %{"ecosystem" => "RUBYGEMS", "name" => name}) do
"https://github.com/#{full_name}/security/dependabot/Gemfile.lock/#{name}/open"
end
defp resolve_issue_url(full_name, %{"ecosystem" => "NPM", "name" => name}) do
"https://github.com/#{full_name}/security/dependabot/yarn.lock/#{name}/open"
end
defp resolve_issue_url(_full_name, _package), do: nil
defp post_to_slack([]), do: IO.puts("Great, no vulnerabilities! Nothing to do here.")
defp post_to_slack(vulnerabilities) do
vulnerabilities
|> slack_message_text()
|> do_post_to_slack()
end
defp do_post_to_slack(text) do
{:ok, _} = HTTPoison.post("https://slack.com/api/chat.postMessage", Jason.encode!(%{channel: @slack_channel, text: text, type: "mrkdwn"}), [{"Authorization","Bearer #{@slack_token}"}, {"Content-Type", "application/json"}])
end
defp slack_message_text(vulnerabilities) do
vulnerabilities
|> Enum.group_by(& &1.package.name)
|> Enum.reduce(["⚠️ Vulnerability alert!\nI found some vulnerabilities in <https://github.com/#{@github_repository}|*#{@github_repository}*>. Please resolve when you have time.\n\n"], fn {package_name, group}, acc ->
[vulnerability | _] = group
message = "<#{vulnerability.resolve_issue_url}|*#{package_name}*>: " <> "#{number_of_vulnerabilities(length(group))}" <> " with severity: #{severities(group)}\n"
message = message <> "ecosystem: #{String.downcase(vulnerability.package.ecosystem || "")}\n"
acc ++ [message]
end)
|> Enum.join("\n")
end
defp number_of_vulnerabilities(size) when size == 0, do: "no vulnerabilities"
defp number_of_vulnerabilities(size) when size == 1, do: "1 vulnerability"
defp number_of_vulnerabilities(size) when size > 1, do: "#{size} vulnerabilities"
defp severities(group) do
group
|> Enum.map(& &1.severity)
|> Enum.uniq()
|> Enum.join(", ")
end
defp parse_datetime(nil), do: nil
defp parse_datetime(""), do: nil
defp parse_datetime(datetime_str) do
case DateTime.from_iso8601(datetime_str) do
{:ok, dt, _offset} -> dt
{:error, _} -> datetime_str
end
end
end
VulnerabilityAlerts.run()