Responding to the Log4shell incident
How an organisation could respond to the Log4Shell (CVE-2021-44228) security vulnerability, if they were using dependency-management-data.
(Note: the Log4Shell security vulnerability predates dependency-management-data, but this is a good opportunity to look into how we could respond to an incident like this)
Context
Via CISA's documentation on Log4Shell:
Log4Shell, disclosed on December 10, 2021, is a remote code execution (RCE) vulnerability affecting Apache’s Log4j library, versions 2.0-beta9 to 2.14.1. The vulnerability exists in the action the Java Naming and Directory Interface (JNDI) takes to resolve variables. Affected versions of Log4j contain JNDI features—such as message lookup substitution—that do not protect against adversary-controlled Lightweight Directory Access Protocol (LDAP), Domain Name System (DNS), and other JNDI-related endpoints.
An adversary can exploit Log4Shell by submitting a specially crafted request to a vulnerable system that causes that system to execute arbitrary code. The request allows the adversary to take full control over the system. The adversary can then steal information, launch ransomware, or conduct other malicious activity.
This was a significantly impactful incident across the industry due to the pervasiveness of Log4J2 being used in many different types of application, from web services, to local developer tools, to toasters!
If an organisation had their dependencies catalogued using a Software Composition Analysis (SCA) platform, or through one of the means that dependency-management-data supports, determining the impact was made much easier.
Problem
- Which repositories depend on
log4j-core
? - Which of those dependencies use an affected version?
- Of those dependencies using the affected version(s), who owns the repo?
- Of those dependencies using the affected version(s), which repos are public?
Data
In the renovate
table, we have:
platform | organisation | repo | package_name | version | current_version | package_manager | package_file_path | datasource | dep_types |
---|---|---|---|---|---|---|---|---|---|
github | elastic | logstash | org.apache.logging.log4j:log4j-core | 2.17.1 | 2.17.1 | gradle | logstash-core/build.gradle | maven | ["dependencies"] |
gitlab | jamietanna | fake-private-repo | org.apache.logging.log4j:log4j-core | 2.13.0 | 2.13.0 | gradle | blank-java/build.gradle | maven | ["dependencies"] |
gitlab | jamietanna | fake-private-repo | org.apache.logging.log4j:log4j-core | 2.13.2 | 2.13.2 | maven | blank-java/pom.xml | maven | ["compile"] |
github | cucumber | cucumber-jvm | org.apache.logging.log4j:log4j-core | 2.17.1 | 2.17.1 | maven | pom.xml | maven | ["build","missing-data"] |
github | elastic | logstash | org.apache.logging.log4j:log4j-core | 2.17.1 | 2.17.1 | gradle | logstash-core/build.gradle | maven | ["dependencies","missing-data"] |
github | elastic | examples | org.apache.logging.log4j:log4j-core | 2.9.1 | 2.9.1 | maven | Search/recipe_search_java/pom.xml | maven | ["compile","missing-data"] |
gitlab | jamietanna | fake-private-repo | org.apache.logging.log4j:log4j-core | 2.13.0 | 2.13.0 | gradle | blank-java/build.gradle | maven | ["dependencies","missing-data"] |
gitlab | jamietanna | fake-private-repo | org.apache.logging.log4j:log4j-core | 2.13.0 | 2.13.0 | maven | blank-java/pom.xml | maven | ["compile","missing-data"] |
gitlab | jamietanna | fake-private-repo | org.apache.logging.log4j:log4j-core | 2.13.0 | 2.13.0 | maven | blank-java/pom.xml | maven | ["build","missing-data"] |
In the owners
table, we have:
platform | organisation | repo | owner | notes | updated_at |
---|---|---|---|---|---|
gitlab | jamietanna | fake-private-repo | Jamie Tanna | 2024-01-18T10:54:05Z | |
github | elastic | examples | Elastic | 2024-01-18T10:54:05Z | |
github | elastic | logstash | Elastic | 2024-01-18T10:54:05Z |
And in the repository_metadata
table:
platform | organisation | repo | is_monorepo | is_fork | repository_type | repository_usage | visibility | description | additional_metadata |
---|---|---|---|---|---|---|---|---|---|
gitlab | jamietanna | fake-private-repo | FALSE | FALSE | service | PRIVATE | |||
github | elastic | examples | FALSE | FALSE | examples | PUBLIC | |||
github | elastic | logstash | FALSE | FALSE | service | PUBLIC |
Query
To start with, we could use the dependenton
report to get a high-level view of all uses of the dependency:
# for Gradle projects
$ dmd report dependenton --db dmd.db --package-manager gradle --package-name org.apache.logging.log4j:log4j-core
+----------+--------------+------------------------------------------+---------+-----------------+---------------------------------+----------------------------+-------------+
| PLATFORM | ORGANISATION | REPO | VERSION | CURRENT VERSION | DEPENDENCY TYPES | FILEPATH | OWNER |
+----------+--------------+------------------------------------------+---------+-----------------+---------------------------------+----------------------------+-------------+
| github | elastic | logstash | 2.17.1 | 2.17.1 | ["dependencies","missing-data"] | logstash-core/build.gradle | Elastic |
| github | elastic | logstash | 2.17.1 | 2.17.1 | ["dependencies"] | logstash-core/build.gradle | Elastic |
| gitlab | jamietanna | fake-private-repo | 2.13.0 | 2.13.0 | ["dependencies","missing-data"] | blank-java/build.gradle | Jamie Tanna |
| gitlab | jamietanna | fake-private-repo | 2.13.0 | 2.13.0 | ["dependencies"] | blank-java/build.gradle | Jamie Tanna |
+----------+--------------+------------------------------------------+---------+-----------------+---------------------------------+----------------------------+-------------+
# for Maven projects
$ dmd report dependenton --db dmd.db --package-manager maven --package-name org.apache.logging.log4j:log4j-core
+----------+--------------+------------------------------------------+---------+-----------------+----------------------------+-----------------------------------+-------------+
| PLATFORM | ORGANISATION | REPO | VERSION | CURRENT VERSION | DEPENDENCY TYPES | FILEPATH | OWNER |
+----------+--------------+------------------------------------------+---------+-----------------+----------------------------+-----------------------------------+-------------+
| github | cucumber | cucumber-jvm | 2.17.1 | 2.17.1 | ["build","missing-data"] | pom.xml | |
| github | elastic | examples | 2.9.1 | 2.9.1 | ["compile","missing-data"] | Search/recipe_search_java/pom.xml | Elastic |
| gitlab | jamietanna | fake-private-repo | 2.13.0 | 2.13.0 | ["build","missing-data"] | blank-java/pom.xml | Jamie Tanna |
| gitlab | jamietanna | fake-private-repo | 2.13.0 | 2.13.0 | ["compile","missing-data"] | blank-java/pom.xml | Jamie Tanna |
| gitlab | jamietanna | fake-private-repo | 2.13.2 | 2.13.2 | ["compile"] | blank-java/pom.xml | Jamie Tanna |
+----------+--------------+------------------------------------------+---------+-----------------+----------------------------+-----------------------------------+-------------+
# and any other JVM ecosystems that you may use
Alternatively, we could use the GraphQL query dependenton
:
{
dependentOn(
packageManager: "gradle"
packageName: "org.apache.logging.log4j:log4j-core"
) {
repositories {
platform
organisation
repo
# other fields could be added, too
}
}
}
Which would return the following results:
{
"data": {
"dependentOn": {
"repositories": [
{
"platform": "github",
"organisation": "elastic",
"repo": "logstash"
},
{
"platform": "github",
"organisation": "elastic",
"repo": "logstash"
},
{
"platform": "gitlab",
"organisation": "jamietanna",
"repo": "fake-private-repo"
},
{
"platform": "gitlab",
"organisation": "jamietanna",
"repo": "fake-private-repo"
}
]
}
}
}
Alternatively, we could write the following query:
select
platform,
organisation,
repo,
current_version
from
renovate
where
package_name = 'org.apache.logging.log4j:log4j-core'
This provides the following data:
platform | organisation | repo | current_version |
---|---|---|---|
github | elastic | logstash | 2.17.1 |
gitlab | jamietanna | fake-private-repo | 2.13.0 |
gitlab | jamietanna | fake-private-repo | 2.13.2 |
github | cucumber | cucumber-jvm | 2.17.1 |
github | elastic | logstash | 2.17.1 |
github | elastic | examples | 2.9.1 |
gitlab | jamietanna | fake-private-repo | 2.13.0 |
gitlab | jamietanna | fake-private-repo | 2.13.0 |
gitlab | jamietanna | fake-private-repo | 2.13.0 |
However, if we wanted to find affected versions, we'd need a SQL query like so:
select
platform,
organisation,
repo,
current_version
from
renovate
where
package_name = 'org.apache.logging.log4j:log4j-core'
and current_version in (
'2.0-beta9', '2.0-rc1',
'2.0-rc2', '2.0.1',
'2.0.2', '2.0',
'2.1', '2.2',
'2.3.1', '2.3.2',
'2.3', '2.4.1',
'2.4', '2.5',
'2.6.1', '2.6.2',
'2.6', '2.7',
'2.8.1', '2.8.2',
'2.8', '2.9.0',
'2.9.1', '2.10.0',
'2.11.0', '2.11.1',
'2.11.2', '2.12.0',
'2.12.1', '2.12.2',
'2.12.3', '2.12.4',
'2.13.0', '2.13.1',
'2.13.2', '2.13.3',
'2.14.0', '2.14.1',
)
This then returns us:
platform | organisation | repo | current_version |
---|---|---|---|
gitlab | jamietanna | fake-private-repo | 2.13.0 |
gitlab | jamietanna | fake-private-repo | 2.13.2 |
github | elastic | examples | 2.9.1 |
gitlab | jamietanna | fake-private-repo | 2.13.0 |
gitlab | jamietanna | fake-private-repo | 2.13.0 |
gitlab | jamietanna | fake-private-repo | 2.13.0 |
And to list the owners:
select
renovate.platform,
renovate.organisation,
renovate.repo,
current_version,
owner
from
renovate
left join owners on renovate.platform = owners.platform
and renovate.organisation = owners.organisation
and renovate.repo = owners.repo
where
package_name = 'org.apache.logging.log4j:log4j-core'
and current_version in (
'2.0-beta9', '2.0-rc1',
'2.0-rc2', '2.0.1',
'2.0.2', '2.0',
'2.1', '2.2',
'2.3.1', '2.3.2',
'2.3', '2.4.1',
'2.4', '2.5',
'2.6.1', '2.6.2',
'2.6', '2.7',
'2.8.1', '2.8.2',
'2.8', '2.9.0',
'2.9.1', '2.10.0',
'2.11.0', '2.11.1',
'2.11.2', '2.12.0',
'2.12.1', '2.12.2',
'2.12.3', '2.12.4',
'2.13.0', '2.13.1',
'2.13.2', '2.13.3',
'2.14.0', '2.14.1',
)
Which then returns:
platform | organisation | repo | current_version | owner |
---|---|---|---|---|
gitlab | jamietanna | fake-private-repo | 2.13.0 | Jamie Tanna |
gitlab | jamietanna | fake-private-repo | 2.13.2 | Jamie Tanna |
github | elastic | examples | 2.9.1 | Elastic |
gitlab | jamietanna | fake-private-repo | 2.13.0 | Jamie Tanna |
gitlab | jamietanna | fake-private-repo | 2.13.0 | Jamie Tanna |
gitlab | jamietanna | fake-private-repo | 2.13.0 | Jamie Tanna |
And finally, prioritise which repositories are public:
select
renovate.platform,
renovate.organisation,
renovate.repo,
current_version,
owner,
(
case visibility when 'PUBLIC' then true
else false
end
) as is_public
from
renovate
left join owners on renovate.platform = owners.platform
and renovate.organisation = owners.organisation
and renovate.repo = owners.repo
left join repository_metadata on renovate.platform = repository_metadata.platform
and renovate.organisation = repository_metadata.organisation
and renovate.repo = repository_metadata.repo
where
package_name = 'org.apache.logging.log4j:log4j-core'
and current_version in (
'2.0-beta9', '2.0-rc1',
'2.0-rc2', '2.0.1',
'2.0.2', '2.0',
'2.1', '2.2',
'2.3.1', '2.3.2',
'2.3', '2.4.1',
'2.4', '2.5',
'2.6.1', '2.6.2',
'2.6', '2.7',
'2.8.1', '2.8.2',
'2.8', '2.9.0',
'2.9.1', '2.10.0',
'2.11.0', '2.11.1',
'2.11.2', '2.12.0',
'2.12.1', '2.12.2',
'2.12.3', '2.12.4',
'2.13.0', '2.13.1',
'2.13.2', '2.13.3',
'2.14.0', '2.14.1',
)
order by is_public desc
Which then produces:
platform | organisation | repo | current_version | owner | is_public |
---|---|---|---|---|---|
gitlab | jamietanna | fake-private-repo | 2.13.0 | Jamie Tanna | 0 |
gitlab | jamietanna | fake-private-repo | 2.13.2 | Jamie Tanna | 0 |
github | elastic | examples | 2.9.1 | Elastic | 1 |
gitlab | jamietanna | fake-private-repo | 2.13.0 | Jamie Tanna | 0 |
gitlab | jamietanna | fake-private-repo | 2.13.0 | Jamie Tanna | 0 |
gitlab | jamietanna | fake-private-repo | 2.13.0 | Jamie Tanna | 0 |
Alternatively, we could leverage writing custom policies which would allow us to write the following policy:
package policy
import rego.v1
default advisory_type := "SECURITY"
# prefer the `version` as versions are usually exactly pinned, and means we don't need to handle the absense of the `current_version` i.e. https://gitlab.com/tanna.dev/dependency-management-data/-/issues/77
versions := split(input.dependency.version, ".")
major := to_number(versions[0])
minor := to_number(versions[1])
patch := to_number(versions[2])
is_log4j2 if {
# this could be avoided, if you want to avoid having an exhaustive list of all possibilities
input.dependency.package_manager in {"gradle", "maven"}
input.dependency.package_name = "org.apache.logging.log4j:log4j-core"
major == 2
}
# CVE-2021-44228 aka Log4shell affects versions 2.0-beta9 to 2.14.1
is_vulnerable_version if input.dependency.version in {"2.0-beta9", "2.0-rc1", "2.0-rc2"}
# CVE-2021-44228 aka Log4shell affects versions 2.0-beta9 to 2.14.1
is_vulnerable_version if {
minor > 0
minor <= 14
}
deny contains msg if {
is_log4j2
is_vulnerable_version
msg := "Dependency is vulnerable to Log4Shell CVE (CVE-2021-44228)"
}
This allows us to simplify the logic for which versions are in-scope, and then outputs the following:
$ dmd policy evaluate --db dmd.db log4shell.rego
Processing log4shell.rego resulted in 6 policy violations, from 146323 dependencies:
+----------+--------------+------------------------------------------+-------------------------------------+----------+---------------------------------+-----------------------------------+-------+---------------+------------------------------------------------------------+
| PLATFORM | ORGANISATION | REPO | PACKAGE | VERSION | DEPENDENCY TYPES | FILEPATH | LEVEL | ADVISORY TYPE | DESCRIPTION |
+----------+--------------+------------------------------------------+-------------------------------------+----------+---------------------------------+-----------------------------------+-------+---------------+------------------------------------------------------------+
| github | elastic | examples | org.apache.logging.log4j:log4j-core | 2.9.1 / | ["compile","missing-data"] | Search/recipe_search_java/pom.xml | ERROR | SECURITY | Dependency is vulnerable to Log4Shell CVE (CVE-2021-44228) |
| | | | | 2.9.1 | | | | | |
| gitlab | jamietanna | localstack-docker-reuse-not-staying-open | org.apache.logging.log4j:log4j-core | 2.13.0 / | ["dependencies","missing-data"] | blank-java/build.gradle | ERROR | SECURITY | Dependency is vulnerable to Log4Shell CVE (CVE-2021-44228) |
| | | | | 2.13.0 | | | | | |
| gitlab | jamietanna | localstack-docker-reuse-not-staying-open | org.apache.logging.log4j:log4j-core | 2.13.0 / | ["dependencies"] | blank-java/build.gradle | ERROR | SECURITY | Dependency is vulnerable to Log4Shell CVE (CVE-2021-44228) |
| | | | | 2.13.0 | | | | | |
| gitlab | jamietanna | localstack-docker-reuse-not-staying-open | org.apache.logging.log4j:log4j-core | 2.13.0 / | ["build","missing-data"] | blank-java/pom.xml | ERROR | SECURITY | Dependency is vulnerable to Log4Shell CVE (CVE-2021-44228) |
| | | | | 2.13.0 | | | | | |
| gitlab | jamietanna | localstack-docker-reuse-not-staying-open | org.apache.logging.log4j:log4j-core | 2.13.2 / | ["compile"] | blank-java/pom.xml | ERROR | SECURITY | Dependency is vulnerable to Log4Shell CVE (CVE-2021-44228) |
| | | | | 2.13.2 | | | | | |
| gitlab | jamietanna | localstack-docker-reuse-not-staying-open | org.apache.logging.log4j:log4j-core | 2.13.0 / | ["compile","missing-data"] | blank-java/pom.xml | ERROR | SECURITY | Dependency is vulnerable to Log4Shell CVE (CVE-2021-44228) |
| | | | | 2.13.0 | | | | | |
+----------+--------------+------------------------------------------+-------------------------------------+----------+---------------------------------+-----------------------------------+-------+---------------+------------------------------------------------------------+