Repository: google/osv-scanner
Pull request #59 (2022-12-15 06:10:07)

- add conan.lock to the list
/cc @prince-chrismc

there are several formats of conan lockfile available in the wild (lockfiles are versioned separately from the conan client itself, as not every conan release changes the format of lockfile):
- initial, unversioned (`0.0`) - added in **conan 1.17**
- `0.1` added in **conan 1.18**, added versioning to the lockfile format
- `0.2` added in **conan 1.21**, added deterministic lockfiles
- `0.3` added in **conan 1.22**, changed how nodes reference each other
- `0.4` added in **conan 1.28**, the format mostly used currently in production and encountered most often 
- `0.5` is used by **conan 2.0+** (currently, as of December 2022, still in beta state, and not yet released)

formats 0.0  - 0.4 are mostly similar and use `graph_lock` with a list of nodes.
format 0.5 is a major overhaul, it's a completely new implementation developed from scratch.

some information about 0.0 - 0.4 lockfiles (current format used in production):
- [doc: lockfiles](https://docs.conan.io/en/latest/versioning/lockfiles.html)
- [doc: using recipe revisions and lockfiles](https://docs.conan.io/en/latest/howtos/using_rrev_and_lockfiles.html)
- [Conan 1.34 : New lockfile feature "bundles", Resource shift to Conan V2.0, Merged Tribe Proposals](https://blog.conan.io/2021/03/10/New-conan-release-1-34.html)
- [Conan 1.32: New validate() Method, First configurable Generator (MSBuildDeps), Renamed Multiple Toolchains & Generators, 2 New Meson Classes, Improve Lockfile Support for Private Requirements, Support for build_requires to Affect package_id (Workaround)](https://blog.conan.io/2020/12/14/New-conan-release-1-32.html)
- [Conan 1.29: Updates to multiple generators, new tools.rename function, experimental toolchain for gnu make, lockfile bug fixes](https://blog.conan.io/2020/09/07/New-conan-release-1-29.html)
- [Conan 1.28: Lockfile improvements, Four new conanfile.py attributes, Improved Clang support on Windows, Support components in pkg_config generator, Define generator filenames](https://blog.conan.io/2020/08/06/New-conan-release-1-28.html)
- [Conan 1.17: The Conan user survey, lockfiles, new graph commands, package ID modes for revisions, apple-clang 11.0 support and usability improvements](https://blog.conan.io/2019/07/15/New-conan-release-1-17.html)

more information about 0.5 lockfiles used in conan 2.0:
- [New 2.0 lockfiles](https://github.com/conan-io/tribe/pull/34)

the format itself is not documented, but can be learnt by studying the source code [graph_lock.py](https://github.com/conan-io/conan/blob/develop/conans/model/graph_lock.py). as it's currently not expected lockfiles to be manually or automatically edited by users or tools, they are only generated by the conan client itself during the graph resolution process, and afterwards, lockfiles are fully immutable.

for the version comparison, conan client itself uses [version ranges](https://docs.conan.io/en/latest/versioning/version_ranges.html) to compare versions, but it's only for packages that follow SemVer (a majority of C and C++ libraries don't follow SemVer, or even predate it). as different libraries use various versioning schemes, conan client cannot make any assumtions.
however, lockfile represents a resolved graph state, with all the version ranges already computed. therefore, versions in lockfiles are compared as regular strings.

prince-chrismc: It needs to be implemented in https://github.com/google/osv-scanner/tree/main/pkg/lockfile 🙊 
SSE4: @prince-chrismc I've added some parser
G-Rath: @SSE4 I've done an initial review and nothing huge has jumped out at me as needing to be changed - I'm not familiar with this ecosystem/tool at all; would you happen to have any references to the lockfile (ideally a spec would be great, but not all ecosystems have those).

It would also be good to know how the version comparison is actually done if you know that too.
SSE4: > @SSE4 I've done an initial review and nothing huge has jumped out at me as needing to be changed - I'm not familiar with this ecosystem/tool at all; would you happen to have any references to the lockfile (ideally a spec would be great, but not all ecosystems have those).
> 
> It would also be good to know how the version comparison is actually done if you know that too.

thanks, I've updated the PR description. lemme know if you have any further question.
SSE4: @G-Rath how do we move this forward? please let me know if you need anything else from my side.
G-Rath: @SSE4 the main thing that needs addressing is [this comment](https://github.com/google/osv-scanner/pull/59/files#r1051688292) - otherwise everything looks ok
SSE4: > @SSE4 the main thing that needs addressing is [this comment](https://github.com/google/osv-scanner/pull/59/files#r1051688292) - otherwise everything looks ok

okay, I've changed ecosystem name `conan.io` to `conan` and added TODO you have suggested.
another-rex: /gcbrun
G-Rath: @SSE4 I might have missed it, but it looks like you don't have a test for when a package exists without a name? if so I think that is important to have, to close off the loop; otherwise LGTM 👍 
SSE4: > @SSE4 I might have missed it, but it looks like you don't have a test for when a package exists without a name? if so I think that is important to have, to close off the loop; otherwise LGTM +1

I'll add some tests with synthetic lockfiles, as I can't generate such with client.
SSE4: > @SSE4 I might have missed it, but it looks like you don't have a test for when a package exists without a name? if so I think that is important to have, to close off the loop; otherwise LGTM +1

now all done
SSE4: /gcbrun
another-rex: /gcbrun
SSE4: @G-Rath can we move this one forward?
SSE4: > Looks good to me, just waiting for ([ossf/osv-schema#101](https://github.com/ossf/osv-schema/pull/101)) to be accepted before merging this in.

it's merged now

Patch:
diff --git a/README.md b/README.md
index 861d1b6..7c9e9c9 100644
--- a/README.md
+++ b/README.md
@@ -129,6 +129,7 @@ A wide range of lockfiles are supported by utilizing this [lockfile package](htt
 - `buildscript-gradle.lockfile`
 - `Cargo.lock`
 - `composer.lock`
+- `conan.lock`
 - `Gemfile.lock`
 - `go.mod`
 - `gradle.lockfile`
diff --git a/internal/semantic/parse.go b/internal/semantic/parse.go
index 99c57c0..871fc03 100644
--- a/internal/semantic/parse.go
+++ b/internal/semantic/parse.go
@@ -41,6 +41,8 @@ func Parse(str string, ecosystem Ecosystem) (Version, error) {
 		return parsePyPIVersion(str), nil
 	case "Pub":
 		return parseSemverVersion(str), nil
+	case "ConanCenter":
+		return parseSemverVersion(str), nil
 	}
 
 	return nil, fmt.Errorf("%w %s", ErrUnsupportedEcosystem, ecosystem)
diff --git a/pkg/lockfile/ecosystems.go b/pkg/lockfile/ecosystems.go
index b4f456d..861373a 100644
--- a/pkg/lockfile/ecosystems.go
+++ b/pkg/lockfile/ecosystems.go
@@ -12,6 +12,7 @@ func KnownEcosystems() []Ecosystem {
 		MavenEcosystem,
 		PipEcosystem,
 		PubEcosystem,
+		ConanEcosystem,
 		// Disabled temporarily,
 		// see https://github.com/google/osv-scanner/pull/128 discussion for additional context
 		// AlpineEcosystem,
diff --git a/pkg/lockfile/fixtures/conan/empty.v1.json b/pkg/lockfile/fixtures/conan/empty.v1.json
new file mode 100644
index 0000000..0ff352b
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/empty.v1.json
@@ -0,0 +1,15 @@
+{
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "ref": "test/1.0",
+       "options": "fPIC=True\nshared=False",
+       "path": "conanfile.py",
+       "context": "host"
+      }
+     },
+     "revisions_enabled": false
+    },
+    "version": "0.4",
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/empty.v1.revisions.json b/pkg/lockfile/fixtures/conan/empty.v1.revisions.json
new file mode 100644
index 0000000..c550aa5
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/empty.v1.revisions.json
@@ -0,0 +1,15 @@
+{
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "ref": "test/1.0",
+       "options": "fPIC=True\nshared=False",
+       "path": "conanfile.py",
+       "context": "host"
+      }
+     },
+     "revisions_enabled": true
+    },
+    "version": "0.4",
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/empty.v2.json b/pkg/lockfile/fixtures/conan/empty.v2.json
new file mode 100644
index 0000000..e97a224
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/empty.v2.json
@@ -0,0 +1,6 @@
+{
+    "version": "0.5",
+    "requires": [],
+    "build_requires": [],
+    "python_requires": []
+}
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/nested-dependencies.v1.json b/pkg/lockfile/fixtures/conan/nested-dependencies.v1.json
new file mode 100644
index 0000000..2974db6
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/nested-dependencies.v1.json
@@ -0,0 +1,62 @@
+{
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "ref": "test/1.0",
+       "options": "fPIC=True\nshared=False\nbrotli:enable_debug=False\nbrotli:enable_log=False\nbrotli:enable_portable=False\nbrotli:enable_rbit=True\nbrotli:endianness=None\nbrotli:fPIC=True\nbrotli:shared=False\nbrotli:target_bits=None\nbzip2:build_executable=True\nbzip2:fPIC=True\nbzip2:shared=False\nfreetype:fPIC=True\nfreetype:shared=False\nfreetype:subpixel=False\nfreetype:with_brotli=True\nfreetype:with_bzip2=True\nfreetype:with_png=True\nfreetype:with_zlib=True\nlibpng:api_prefix=\nlibpng:fPIC=True\nlibpng:shared=False\nlibpng:sse=True\nzlib:fPIC=True\nzlib:shared=False",
+       "requires": [
+        "1"
+       ],
+       "path": "conanfile.py",
+       "context": "host"
+      },
+      "1": {
+       "ref": "freetype/2.12.1",
+       "options": "fPIC=True\nshared=False\nsubpixel=False\nwith_brotli=True\nwith_bzip2=True\nwith_png=True\nwith_zlib=True\nbrotli:enable_debug=False\nbrotli:enable_log=False\nbrotli:enable_portable=False\nbrotli:enable_rbit=True\nbrotli:endianness=None\nbrotli:fPIC=True\nbrotli:shared=False\nbrotli:target_bits=None\nbzip2:build_executable=True\nbzip2:fPIC=True\nbzip2:shared=False\nlibpng:api_prefix=\nlibpng:fPIC=True\nlibpng:shared=False\nlibpng:sse=True\nzlib:fPIC=True\nzlib:shared=False",
+       "package_id": "bca7b8880d98719d556dd526ce612be20a815922",
+       "prev": "0",
+       "requires": [
+        "2",
+        "3",
+        "4",
+        "5"
+       ],
+       "context": "host"
+      },
+      "2": {
+       "ref": "libpng/1.6.39",
+       "options": "api_prefix=\nfPIC=True\nshared=False\nsse=True\nzlib:fPIC=True\nzlib:shared=False",
+       "package_id": "d5b3dc27faecfb4eb94086722000dd65bb9e6bff",
+       "prev": "0",
+       "requires": [
+        "3"
+       ],
+       "context": "host"
+      },
+      "3": {
+       "ref": "zlib/1.2.13",
+       "options": "fPIC=True\nshared=False",
+       "package_id": "19729b9559f3ae196cad45cb2b97468ccb75dcd1",
+       "prev": "0",
+       "context": "host"
+      },
+      "4": {
+       "ref": "bzip2/1.0.8",
+       "options": "build_executable=True\nfPIC=True\nshared=False",
+       "package_id": "91a8b22c2c5a149bc617cfc06cdd21bf23b12567",
+       "prev": "0",
+       "context": "host"
+      },
+      "5": {
+       "ref": "brotli/1.0.9",
+       "options": "enable_debug=False\nenable_log=False\nenable_portable=False\nenable_rbit=True\nendianness=None\nfPIC=True\nshared=False\ntarget_bits=None",
+       "package_id": "bfdbb855937046dc347fec082c59cb7f733e8855",
+       "prev": "0",
+       "context": "host"
+      }
+     },
+     "revisions_enabled": false
+    },
+    "version": "0.4",
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/nested-dependencies.v1.revisions.json b/pkg/lockfile/fixtures/conan/nested-dependencies.v1.revisions.json
new file mode 100644
index 0000000..12165b1
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/nested-dependencies.v1.revisions.json
@@ -0,0 +1,62 @@
+{
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "ref": "test/1.0",
+       "options": "fPIC=True\nshared=False\nbrotli:enable_debug=False\nbrotli:enable_log=False\nbrotli:enable_portable=False\nbrotli:enable_rbit=True\nbrotli:endianness=None\nbrotli:fPIC=True\nbrotli:shared=False\nbrotli:target_bits=None\nbzip2:build_executable=True\nbzip2:fPIC=True\nbzip2:shared=False\nfreetype:fPIC=True\nfreetype:shared=False\nfreetype:subpixel=False\nfreetype:with_brotli=True\nfreetype:with_bzip2=True\nfreetype:with_png=True\nfreetype:with_zlib=True\nlibpng:api_prefix=\nlibpng:fPIC=True\nlibpng:shared=False\nlibpng:sse=True\nzlib:fPIC=True\nzlib:shared=False",
+       "requires": [
+        "1"
+       ],
+       "path": "conanfile.py",
+       "context": "host"
+      },
+      "1": {
+       "ref": "freetype/2.12.1#7e1b67634f54f38a979bbad44fd09a2c",
+       "options": "fPIC=True\nshared=False\nsubpixel=False\nwith_brotli=True\nwith_bzip2=True\nwith_png=True\nwith_zlib=True\nbrotli:enable_debug=False\nbrotli:enable_log=False\nbrotli:enable_portable=False\nbrotli:enable_rbit=True\nbrotli:endianness=None\nbrotli:fPIC=True\nbrotli:shared=False\nbrotli:target_bits=None\nbzip2:build_executable=True\nbzip2:fPIC=True\nbzip2:shared=False\nlibpng:api_prefix=\nlibpng:fPIC=True\nlibpng:shared=False\nlibpng:sse=True\nzlib:fPIC=True\nzlib:shared=False",
+       "package_id": "bca7b8880d98719d556dd526ce612be20a815922",
+       "prev": "400c9a65b20f791ea05c47eb6817e80a",
+       "requires": [
+        "2",
+        "3",
+        "4",
+        "5"
+       ],
+       "context": "host"
+      },
+      "2": {
+       "ref": "libpng/1.6.39#7927e8ce5b2576a6ea497c6ca70e9751",
+       "options": "api_prefix=\nfPIC=True\nshared=False\nsse=True\nzlib:fPIC=True\nzlib:shared=False",
+       "package_id": "d5b3dc27faecfb4eb94086722000dd65bb9e6bff",
+       "prev": "3e3b7f79b03c52ab932089560ea2eb56",
+       "requires": [
+        "3"
+       ],
+       "context": "host"
+      },
+      "3": {
+       "ref": "zlib/1.2.13#13c96f538b52e1600c40b88994de240f",
+       "options": "fPIC=True\nshared=False",
+       "package_id": "19729b9559f3ae196cad45cb2b97468ccb75dcd1",
+       "prev": "562e6cc3d7987119418780e5c5697342",
+       "context": "host"
+      },
+      "4": {
+       "ref": "bzip2/1.0.8#464be69744fa6d48ed01928cfe470008",
+       "options": "build_executable=True\nfPIC=True\nshared=False",
+       "package_id": "91a8b22c2c5a149bc617cfc06cdd21bf23b12567",
+       "prev": "94d2f51be78e63879215a3b2ba014fda",
+       "context": "host"
+      },
+      "5": {
+       "ref": "brotli/1.0.9#4bfbb302b87df342ccd6a2b5fdad307a",
+       "options": "enable_debug=False\nenable_log=False\nenable_portable=False\nenable_rbit=True\nendianness=None\nfPIC=True\nshared=False\ntarget_bits=None",
+       "package_id": "bfdbb855937046dc347fec082c59cb7f733e8855",
+       "prev": "c2eaa7784f2988c35d8b8925a783e73b",
+       "context": "host"
+      }
+     },
+     "revisions_enabled": true
+    },
+    "version": "0.4",
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/nested-dependencies.v2.json b/pkg/lockfile/fixtures/conan/nested-dependencies.v2.json
new file mode 100644
index 0000000..87b2bbd
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/nested-dependencies.v2.json
@@ -0,0 +1,12 @@
+{
+    "version": "0.5",
+    "requires": [
+        "zlib/1.2.13#13c96f538b52e1600c40b88994de240f%1667396813.733",
+        "libpng/1.6.39#7927e8ce5b2576a6ea497c6ca70e9751%1669038072.946",
+        "freetype/2.12.1#7e1b67634f54f38a979bbad44fd09a2c%1669913185.923",
+        "bzip2/1.0.8#464be69744fa6d48ed01928cfe470008%1666580345.213",
+        "brotli/1.0.9#4bfbb302b87df342ccd6a2b5fdad307a%1661519995.45"
+    ],
+    "build_requires": [],
+    "python_requires": []
+}
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/no-name.v1.json b/pkg/lockfile/fixtures/conan/no-name.v1.json
new file mode 100644
index 0000000..f7494ee
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/no-name.v1.json
@@ -0,0 +1,32 @@
+{
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "ref": "test/1.0",
+       "options": "fPIC=True\nshared=False\nzlib:fPIC=True\nzlib:shared=False",
+       "requires": [
+        "1", "2"
+       ],
+       "path": "conanfile.py",
+       "context": "host"
+      },
+      "1": {
+       "ref": "zlib/1.2.11",
+       "options": "fPIC=True\nshared=False",
+       "package_id": "19729b9559f3ae196cad45cb2b97468ccb75dcd1",
+       "prev": "0",
+       "context": "host"
+      },
+      "2": {
+        "ref": "1.2.3",
+        "options": "fPIC=True\nshared=False",
+        "package_id": "19729b9559f3ae196cad45cb2b97468ccb75dcd1",
+        "prev": "0",
+        "context": "host"
+       }
+     },
+     "revisions_enabled": false
+    },
+    "version": "0.4",
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/no-name.v1.revisions.json b/pkg/lockfile/fixtures/conan/no-name.v1.revisions.json
new file mode 100644
index 0000000..5b13d8e
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/no-name.v1.revisions.json
@@ -0,0 +1,32 @@
+{
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "ref": "test/1.0",
+       "options": "fPIC=True\nshared=False\nzlib:fPIC=True\nzlib:shared=False",
+       "requires": [
+        "1", "2"
+       ],
+       "path": "conanfile.py",
+       "context": "host"
+      },
+      "1": {
+       "ref": "zlib/1.2.11#ffa77daf83a57094149707928bdce823",
+       "options": "fPIC=True\nshared=False",
+       "package_id": "19729b9559f3ae196cad45cb2b97468ccb75dcd1",
+       "prev": "a636df1594de20e55e1c393ffb1eb166",
+       "context": "host"
+      },
+      "2": {
+        "ref": "1.2.3#ffa77daf83a57094149707928bdce823",
+        "options": "fPIC=True\nshared=False",
+        "package_id": "19729b9559f3ae196cad45cb2b97468ccb75dcd1",
+        "prev": "a636df1594de20e55e1c393ffb1eb166",
+        "context": "host"
+       }
+     },
+     "revisions_enabled": true
+    },
+    "version": "0.4",
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/no-name.v2.json b/pkg/lockfile/fixtures/conan/no-name.v2.json
new file mode 100644
index 0000000..eca2989
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/no-name.v2.json
@@ -0,0 +1,9 @@
+{
+    "version": "0.5",
+    "requires": [
+        "zlib/1.2.11#ffa77daf83a57094149707928bdce823%1667396813.184",
+        "1.2.3#ffa77daf83a57094149707928bdce823%1667396813.184"
+    ],
+    "build_requires": [],
+    "python_requires": []
+}
diff --git a/pkg/lockfile/fixtures/conan/not-json.txt b/pkg/lockfile/fixtures/conan/not-json.txt
new file mode 100644
index 0000000..3ae3a21
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/not-json.txt
@@ -0,0 +1,1 @@
+this is not json!
diff --git a/pkg/lockfile/fixtures/conan/old-format-0.0.json b/pkg/lockfile/fixtures/conan/old-format-0.0.json
new file mode 100644
index 0000000..fcbbcac
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/old-format-0.0.json
@@ -0,0 +1,18 @@
+{
+    "profile": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n",
+    "graph_lock": {
+     "nodes": {
+      "05b715be-7ec7-11ed-8a66-b537134a228d": {
+       "pref": null,
+       "options": "zlib:fPIC=True\nzlib:minizip=False\nzlib:shared=False",
+       "requires": {
+        "zlib/1.2.11@bincrafters/testing#5f4917ce0a630b102f472afd00102d40": "05b715bd-7ec7-11ed-8a66-b537134a228d"
+       }
+      },
+      "05b715bd-7ec7-11ed-8a66-b537134a228d": {
+       "pref": "zlib/1.2.11@bincrafters/testing#5f4917ce0a630b102f472afd00102d40:19729b9559f3ae196cad45cb2b97468ccb75dcd1#58846c4ed127f63e9c88c5be5190a6d9",
+       "options": "fPIC=True\nminizip=False\nshared=False"
+      }
+     }
+    }
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/old-format-0.1.json b/pkg/lockfile/fixtures/conan/old-format-0.1.json
new file mode 100644
index 0000000..b9b41e4
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/old-format-0.1.json
@@ -0,0 +1,20 @@
+{
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n",
+    "graph_lock": {
+     "nodes": {
+      "1058d05a-7ec6-11ed-8a66-b537134a228d": {
+       "pref": "test/1.0:2ce08bf790c58b729dda567426e810ed5e35e513",
+       "options": "fPIC=True\nshared=False\nzlib:fPIC=True\nzlib:minizip=False\nzlib:shared=False",
+       "requires": {
+        "zlib/1.2.11#5f4917ce0a630b102f472afd00102d40": "1058d059-7ec6-11ed-8a66-b537134a228d"
+       },
+       "path": "/home/sse4/projects/conan_test/v1/conanfile.py"
+      },
+      "1058d059-7ec6-11ed-8a66-b537134a228d": {
+       "pref": "zlib/1.2.11#5f4917ce0a630b102f472afd00102d40:19729b9559f3ae196cad45cb2b97468ccb75dcd1#58846c4ed127f63e9c88c5be5190a6d9",
+       "options": "fPIC=True\nminizip=False\nshared=False"
+      }
+     }
+    },
+    "version": "0.1"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/old-format-0.2.json b/pkg/lockfile/fixtures/conan/old-format-0.2.json
new file mode 100644
index 0000000..e62dbf7
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/old-format-0.2.json
@@ -0,0 +1,20 @@
+{
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n",
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "pref": "test/1.0:2ce08bf790c58b729dda567426e810ed5e35e513",
+       "options": "fPIC=True\nshared=False\nzlib:fPIC=True\nzlib:minizip=False\nzlib:shared=False",
+       "requires": {
+        "zlib/1.2.11#5f4917ce0a630b102f472afd00102d40": "1"
+       },
+       "path": "/home/sse4/projects/conan_test/v1/conanfile.py"
+      },
+      "1": {
+       "pref": "zlib/1.2.11#5f4917ce0a630b102f472afd00102d40:19729b9559f3ae196cad45cb2b97468ccb75dcd1#58846c4ed127f63e9c88c5be5190a6d9",
+       "options": "fPIC=True\nminizip=False\nshared=False"
+      }
+     }
+    },
+    "version": "0.2"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/old-format-0.3.json b/pkg/lockfile/fixtures/conan/old-format-0.3.json
new file mode 100644
index 0000000..fe9136b
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/old-format-0.3.json
@@ -0,0 +1,21 @@
+{
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n",
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "pref": "test/1.0:2ce08bf790c58b729dda567426e810ed5e35e513",
+       "options": "fPIC=True\nshared=False\nzlib:fPIC=True\nzlib:minizip=False\nzlib:shared=False",
+       "requires": [
+        "1"
+       ],
+       "path": "/home/sse4/projects/conan_test/v1/conanfile.py"
+      },
+      "1": {
+       "pref": "zlib/1.2.11#5f4917ce0a630b102f472afd00102d40:19729b9559f3ae196cad45cb2b97468ccb75dcd1#58846c4ed127f63e9c88c5be5190a6d9",
+       "options": "fPIC=True\nminizip=False\nshared=False",
+       "modified": "built"
+      }
+     }
+    },
+    "version": "0.3"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/one-package-dev.v1.json b/pkg/lockfile/fixtures/conan/one-package-dev.v1.json
new file mode 100644
index 0000000..d99f9be
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/one-package-dev.v1.json
@@ -0,0 +1,25 @@
+{
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "ref": "test/1.0",
+       "options": "fPIC=True\nshared=False",
+       "build_requires": [
+        "1"
+       ],
+       "path": "conanfile.py",
+       "context": "host"
+      },
+      "1": {
+       "ref": "ninja/1.11.1",
+       "options": "",
+       "package_id": "24647d9fe8ec489125dfbae4b3ebefaf7581674c",
+       "prev": "0",
+       "context": "host"
+      }
+     },
+     "revisions_enabled": false
+    },
+    "version": "0.4",
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/one-package-dev.v1.revisions.json b/pkg/lockfile/fixtures/conan/one-package-dev.v1.revisions.json
new file mode 100644
index 0000000..9247e54
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/one-package-dev.v1.revisions.json
@@ -0,0 +1,25 @@
+{
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "ref": "test/1.0",
+       "options": "fPIC=True\nshared=False",
+       "build_requires": [
+        "1"
+       ],
+       "path": "conanfile.py",
+       "context": "host"
+      },
+      "1": {
+       "ref": "ninja/1.11.1#a2f0b832705907016f336839f96963f8",
+       "options": "",
+       "package_id": "24647d9fe8ec489125dfbae4b3ebefaf7581674c",
+       "prev": "d894345ae9996c9b97f1cb4150051c25",
+       "context": "host"
+      }
+     },
+     "revisions_enabled": true
+    },
+    "version": "0.4",
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/one-package-dev.v2.json b/pkg/lockfile/fixtures/conan/one-package-dev.v2.json
new file mode 100644
index 0000000..e806573
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/one-package-dev.v2.json
@@ -0,0 +1,8 @@
+{
+    "version": "0.5",
+    "requires": [],
+    "build_requires": [
+        "ninja/1.11.1#a2f0b832705907016f336839f96963f8%1667050636.338"
+    ],
+    "python_requires": []
+}
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/one-package.v1.json b/pkg/lockfile/fixtures/conan/one-package.v1.json
new file mode 100644
index 0000000..f5b9602
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/one-package.v1.json
@@ -0,0 +1,25 @@
+{
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "ref": "test/1.0",
+       "options": "fPIC=True\nshared=False\nzlib:fPIC=True\nzlib:shared=False",
+       "requires": [
+        "1"
+       ],
+       "path": "conanfile.py",
+       "context": "host"
+      },
+      "1": {
+       "ref": "zlib/1.2.11",
+       "options": "fPIC=True\nshared=False",
+       "package_id": "19729b9559f3ae196cad45cb2b97468ccb75dcd1",
+       "prev": "0",
+       "context": "host"
+      }
+     },
+     "revisions_enabled": false
+    },
+    "version": "0.4",
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/one-package.v1.revisions.json b/pkg/lockfile/fixtures/conan/one-package.v1.revisions.json
new file mode 100644
index 0000000..755d954
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/one-package.v1.revisions.json
@@ -0,0 +1,25 @@
+{
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "ref": "test/1.0",
+       "options": "fPIC=True\nshared=False\nzlib:fPIC=True\nzlib:shared=False",
+       "requires": [
+        "1"
+       ],
+       "path": "conanfile.py",
+       "context": "host"
+      },
+      "1": {
+       "ref": "zlib/1.2.11#ffa77daf83a57094149707928bdce823",
+       "options": "fPIC=True\nshared=False",
+       "package_id": "19729b9559f3ae196cad45cb2b97468ccb75dcd1",
+       "prev": "a636df1594de20e55e1c393ffb1eb166",
+       "context": "host"
+      }
+     },
+     "revisions_enabled": true
+    },
+    "version": "0.4",
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/one-package.v2.json b/pkg/lockfile/fixtures/conan/one-package.v2.json
new file mode 100644
index 0000000..4089506
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/one-package.v2.json
@@ -0,0 +1,8 @@
+{
+    "version": "0.5",
+    "requires": [
+        "zlib/1.2.11#ffa77daf83a57094149707928bdce823%1667396813.184"
+    ],
+    "build_requires": [],
+    "python_requires": []
+}
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/two-packages.v1.json b/pkg/lockfile/fixtures/conan/two-packages.v1.json
new file mode 100644
index 0000000..6fad91e
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/two-packages.v1.json
@@ -0,0 +1,33 @@
+{
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "ref": "test/1.0",
+       "options": "fPIC=True\nshared=False\nbzip2:build_executable=True\nbzip2:fPIC=True\nbzip2:shared=False\nzlib:fPIC=True\nzlib:shared=False",
+       "requires": [
+        "1",
+        "2"
+       ],
+       "path": "conanfile.py",
+       "context": "host"
+      },
+      "1": {
+       "ref": "zlib/1.2.11",
+       "options": "fPIC=True\nshared=False",
+       "package_id": "19729b9559f3ae196cad45cb2b97468ccb75dcd1",
+       "prev": "0",
+       "context": "host"
+      },
+      "2": {
+       "ref": "bzip2/1.0.8",
+       "options": "build_executable=True\nfPIC=True\nshared=False",
+       "package_id": "91a8b22c2c5a149bc617cfc06cdd21bf23b12567",
+       "prev": "0",
+       "context": "host"
+      }
+     },
+     "revisions_enabled": false
+    },
+    "version": "0.4",
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/two-packages.v1.revisions.json b/pkg/lockfile/fixtures/conan/two-packages.v1.revisions.json
new file mode 100644
index 0000000..3306ffc
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/two-packages.v1.revisions.json
@@ -0,0 +1,33 @@
+{
+    "graph_lock": {
+     "nodes": {
+      "0": {
+       "ref": "test/1.0",
+       "options": "fPIC=True\nshared=False\nbzip2:build_executable=True\nbzip2:fPIC=True\nbzip2:shared=False\nzlib:fPIC=True\nzlib:shared=False",
+       "requires": [
+        "1",
+        "2"
+       ],
+       "path": "conanfile.py",
+       "context": "host"
+      },
+      "1": {
+       "ref": "zlib/1.2.11#ffa77daf83a57094149707928bdce823",
+       "options": "fPIC=True\nshared=False",
+       "package_id": "19729b9559f3ae196cad45cb2b97468ccb75dcd1",
+       "prev": "a636df1594de20e55e1c393ffb1eb166",
+       "context": "host"
+      },
+      "2": {
+       "ref": "bzip2/1.0.8#464be69744fa6d48ed01928cfe470008",
+       "options": "build_executable=True\nfPIC=True\nshared=False",
+       "package_id": "91a8b22c2c5a149bc617cfc06cdd21bf23b12567",
+       "prev": "94d2f51be78e63879215a3b2ba014fda",
+       "context": "host"
+      }
+     },
+     "revisions_enabled": true
+    },
+    "version": "0.4",
+    "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=10\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
+   }
\ No newline at end of file
diff --git a/pkg/lockfile/fixtures/conan/two-packages.v2.json b/pkg/lockfile/fixtures/conan/two-packages.v2.json
new file mode 100644
index 0000000..6d9aa3d
--- /dev/null
+++ b/pkg/lockfile/fixtures/conan/two-packages.v2.json
@@ -0,0 +1,9 @@
+{
+    "version": "0.5",
+    "requires": [
+        "zlib/1.2.11#ffa77daf83a57094149707928bdce823%1667396813.184",
+        "bzip2/1.0.8#464be69744fa6d48ed01928cfe470008%1666580345.213"
+    ],
+    "build_requires": [],
+    "python_requires": []
+}
\ No newline at end of file
diff --git a/pkg/lockfile/parse-conan-lock.go b/pkg/lockfile/parse-conan-lock.go
new file mode 100644
index 0000000..c10fcba
--- /dev/null
+++ b/pkg/lockfile/parse-conan-lock.go
@@ -0,0 +1,188 @@
+package lockfile
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"strings"
+)
+
+type ConanReference struct {
+	Name            string
+	Version         string
+	Username        string
+	Channel         string
+	RecipeRevision  string
+	PackageID       string
+	PackageRevision string
+	TimeStamp       string
+}
+
+type ConanGraphNode struct {
+	Pref      string `json:"pref"`
+	Ref       string `json:"ref"`
+	Options   string `json:"options"`
+	PackageId string `json:"package_id"`
+	Prev      string `json:"prev"`
+	Path      string `json:"path"`
+	Context   string `json:"context"`
+}
+
+type ConanGraphLock struct {
+	Nodes map[string]ConanGraphNode `json:"nodes"`
+}
+
+type ConanLockFile struct {
+	Version string `json:"version"`
+	// conan v0.4- lockfiles use "graph_lock", "profile_host" and "profile_build"
+	GraphLock    ConanGraphLock `json:"graph_lock,omitempty"`
+	ProfileHost  string         `json:"profile_host,omitempty"`
+	ProfileBuild string         `json:"profile_build,omitempty"`
+	// conan v0.5+ lockfiles use "requires", "build_requires" and "python_requires"
+	Requires       []string `json:"requires,omitempty"`
+	BuildRequires  []string `json:"build_requires,omitempty"`
+	PythonRequires []string `json:"python_requires,omitempty"`
+}
+
+// TODO this is tentative and subject to change depending on the OSV schema
+const ConanEcosystem Ecosystem = "ConanCenter"
+
+func parseConanRenference(ref string) ConanReference {
+	// very flexible format name/version[@username[/channel]][#rrev][:pkgid[#prev]][%timestamp]
+	var reference ConanReference
+
+	parts := strings.SplitN(ref, "%", 2)
+	if len(parts) == 2 {
+		ref = parts[0]
+		reference.TimeStamp = parts[1]
+	}
+
+	parts = strings.SplitN(ref, ":", 2)
+	if len(parts) == 2 {
+		ref = parts[0]
+		parts = strings.SplitN(parts[1], "#", 2)
+		reference.PackageID = parts[0]
+		if len(parts) == 2 {
+			reference.PackageRevision = parts[1]
+		}
+	}
+
+	parts = strings.SplitN(ref, "#", 2)
+	if len(parts) == 2 {
+		ref = parts[0]
+		reference.RecipeRevision = parts[1]
+	}
+
+	parts = strings.SplitN(ref, "@", 2)
+	if len(parts) == 2 {
+		ref = parts[0]
+		UsernameChannel := parts[1]
+
+		parts = strings.SplitN(UsernameChannel, "/", 2)
+		reference.Username = parts[0]
+		if len(parts) == 2 {
+			reference.Channel = parts[1]
+		}
+	}
+
+	parts = strings.SplitN(ref, "/", 2)
+	if len(parts) == 2 {
+		reference.Name = parts[0]
+		reference.Version = parts[1]
+	} else {
+		// consumer conanfile.txt or conanfile.py might not have a name
+		reference.Name = ""
+		reference.Version = ref
+	}
+	return reference
+}
+
+func parseConanV1Lock(lockfile ConanLockFile) []PackageDetails {
+	var reference ConanReference
+	packages := make([]PackageDetails, 0, len(lockfile.GraphLock.Nodes))
+
+	for _, node := range lockfile.GraphLock.Nodes {
+		if node.Path != "" {
+			// a local "conanfile.txt", skip
+			continue
+		}
+
+		if node.Pref != "" {
+			// old format 0.3 (conan 1.27-) lockfiles use "pref" instead of "ref"
+			reference = parseConanRenference(node.Pref)
+		} else if node.Ref != "" {
+			reference = parseConanRenference(node.Ref)
+		} else {
+			continue
+		}
+		// skip entries with no name, they are most likely consumer's conanfiles
+		// and not dependencies to be searched in a database anyway
+		if reference.Name == "" {
+			continue
+		}
+		packages = append(packages, PackageDetails{
+			Name:      reference.Name,
+			Version:   reference.Version,
+			Ecosystem: ConanEcosystem,
+			CompareAs: ConanEcosystem,
+		})
+	}
+
+	return packages
+}
+
+func parseConanRequires(packages *[]PackageDetails, requires []string) {
+	for _, ref := range requires {
+		reference := parseConanRenference(ref)
+		// skip entries with no name, they are most likely consumer's conanfiles
+		// and not dependencies to be searched in a database anyway
+		if reference.Name == "" {
+			continue
+		}
+
+		*packages = append(*packages, PackageDetails{
+			Name:      reference.Name,
+			Version:   reference.Version,
+			Ecosystem: ConanEcosystem,
+			CompareAs: ConanEcosystem,
+		})
+	}
+}
+
+func parseConanV2Lock(lockfile ConanLockFile) []PackageDetails {
+	packages := make(
+		[]PackageDetails,
+		0,
+		uint64(len(lockfile.Requires))+uint64(len(lockfile.BuildRequires))+uint64(len(lockfile.PythonRequires)),
+	)
+
+	parseConanRequires(&packages, lockfile.Requires)
+	parseConanRequires(&packages, lockfile.BuildRequires)
+	parseConanRequires(&packages, lockfile.PythonRequires)
+
+	return packages
+}
+
+func parseConanLock(lockfile ConanLockFile) []PackageDetails {
+	if lockfile.GraphLock.Nodes != nil {
+		return parseConanV1Lock(lockfile)
+	}
+
+	return parseConanV2Lock(lockfile)
+}
+
+func ParseConanLock(pathToLockfile string) ([]PackageDetails, error) {
+	var parsedLockfile *ConanLockFile
+
+	lockfileContents, err := os.ReadFile(pathToLockfile)
+	if err != nil {
+		return []PackageDetails{}, fmt.Errorf("could not read %s: %w", pathToLockfile, err)
+	}
+
+	err = json.Unmarshal(lockfileContents, &parsedLockfile)
+	if err != nil {
+		return []PackageDetails{}, fmt.Errorf("could not parse %s: %w", pathToLockfile, err)
+	}
+
+	return parseConanLock(*parsedLockfile), nil
+}
diff --git a/pkg/lockfile/parse.go b/pkg/lockfile/parse.go
index 3a34b22..0fe9da4 100644
--- a/pkg/lockfile/parse.go
+++ b/pkg/lockfile/parse.go
@@ -21,6 +21,7 @@ var parsers = map[string]PackageDetailsParser{
 	"buildscript-gradle.lockfile": ParseGradleLock,
 	"Cargo.lock":                  ParseCargoLock,
 	"composer.lock":               ParseComposerLock,
+	"conan.lock":                  ParseConanLock,
 	"Gemfile.lock":                ParseGemfileLock,
 	"go.mod":                      ParseGoLock,
 	"gradle.lockfile":             ParseGradleLock,


Test patch:
diff --git a/pkg/lockfile/parse-conan-lock-v1-revisions_test.go b/pkg/lockfile/parse-conan-lock-v1-revisions_test.go
new file mode 100644
index 0000000..5d649fb
--- /dev/null
+++ b/pkg/lockfile/parse-conan-lock-v1-revisions_test.go
@@ -0,0 +1,162 @@
+package lockfile_test
+
+import (
+	"testing"
+
+	"github.com/google/osv-scanner/pkg/lockfile"
+)
+
+func TestParseConanLock_v1_revisions_FileDoesNotExist(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/does-not-exist")
+
+	expectErrContaining(t, err, "could not read")
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
+
+func TestParseConanLock_v1_revisions_InvalidJson(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/not-json.txt")
+
+	expectErrContaining(t, err, "could not parse")
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
+
+func TestParseConanLock_v1_revisions_NoPackages(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/empty.v1.revisions.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
+
+func TestParseConanLock_v1_revisions_OnePackage(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/one-package.v1.revisions.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v1_revisions_NoName(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/no-name.v1.revisions.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v1_revisions_TwoPackages(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/two-packages.v1.revisions.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "bzip2",
+			Version:   "1.0.8",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v1_revisions_NestedDependencies(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/nested-dependencies.v1.revisions.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.13",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "bzip2",
+			Version:   "1.0.8",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "freetype",
+			Version:   "2.12.1",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "libpng",
+			Version:   "1.6.39",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "brotli",
+			Version:   "1.0.9",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v1_revisions_OnePackageDev(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/one-package-dev.v1.revisions.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "ninja",
+			Version:   "1.11.1",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
diff --git a/pkg/lockfile/parse-conan-lock-v1_test.go b/pkg/lockfile/parse-conan-lock-v1_test.go
new file mode 100644
index 0000000..e3bc280
--- /dev/null
+++ b/pkg/lockfile/parse-conan-lock-v1_test.go
@@ -0,0 +1,238 @@
+package lockfile_test
+
+import (
+	"testing"
+
+	"github.com/google/osv-scanner/pkg/lockfile"
+)
+
+func TestParseConanLock_v1_FileDoesNotExist(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/does-not-exist")
+
+	expectErrContaining(t, err, "could not read")
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
+
+func TestParseConanLock_v1_InvalidJson(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/not-json.txt")
+
+	expectErrContaining(t, err, "could not parse")
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
+
+func TestParseConanLock_v1_NoPackages(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/empty.v1.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
+
+func TestParseConanLock_v1_OnePackage(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/one-package.v1.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v1_NoName(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/no-name.v1.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v1_TwoPackages(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/two-packages.v1.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "bzip2",
+			Version:   "1.0.8",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v1_NestedDependencies(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/nested-dependencies.v1.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.13",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "bzip2",
+			Version:   "1.0.8",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "freetype",
+			Version:   "2.12.1",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "libpng",
+			Version:   "1.6.39",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "brotli",
+			Version:   "1.0.9",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v1_OnePackageDev(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/one-package-dev.v1.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "ninja",
+			Version:   "1.11.1",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v1_OldFormat00(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/old-format-0.0.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v1_OldFormat01(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/old-format-0.1.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v1_OldFormat02(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/old-format-0.2.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v1_OldFormat03(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/old-format-0.3.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
diff --git a/pkg/lockfile/parse-conan-lock-v2_test.go b/pkg/lockfile/parse-conan-lock-v2_test.go
new file mode 100644
index 0000000..873157b
--- /dev/null
+++ b/pkg/lockfile/parse-conan-lock-v2_test.go
@@ -0,0 +1,162 @@
+package lockfile_test
+
+import (
+	"testing"
+
+	"github.com/google/osv-scanner/pkg/lockfile"
+)
+
+func TestParseConanLock_v2_FileDoesNotExist(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/does-not-exist")
+
+	expectErrContaining(t, err, "could not read")
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
+
+func TestParseConanLock_v2_InvalidJson(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/not-json.txt")
+
+	expectErrContaining(t, err, "could not parse")
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
+
+func TestParseConanLock_v2_NoPackages(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/empty.v2.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{})
+}
+
+func TestParseConanLock_v2_OnePackage(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/one-package.v2.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v2_NoName(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/no-name.v2.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v2_TwoPackages(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/two-packages.v2.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.11",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "bzip2",
+			Version:   "1.0.8",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v2_NestedDependencies(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/nested-dependencies.v2.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "zlib",
+			Version:   "1.2.13",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "bzip2",
+			Version:   "1.0.8",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "freetype",
+			Version:   "2.12.1",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "libpng",
+			Version:   "1.6.39",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+		{
+			Name:      "brotli",
+			Version:   "1.0.9",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
+
+func TestParseConanLock_v2_OnePackageDev(t *testing.T) {
+	t.Parallel()
+
+	packages, err := lockfile.ParseConanLock("fixtures/conan/one-package-dev.v2.json")
+
+	if err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+
+	expectPackages(t, packages, []lockfile.PackageDetails{
+		{
+			Name:      "ninja",
+			Version:   "1.11.1",
+			Ecosystem: lockfile.ConanEcosystem,
+			CompareAs: lockfile.ConanEcosystem,
+		},
+	})
+}
diff --git a/pkg/lockfile/parse_test.go b/pkg/lockfile/parse_test.go
index 9bf9cad..1d74be8 100644
--- a/pkg/lockfile/parse_test.go
+++ b/pkg/lockfile/parse_test.go
@@ -93,6 +93,7 @@ func TestParse_FindsExpectedParsers(t *testing.T) {
 		"buildscript-gradle.lockfile",
 		"Cargo.lock",
 		"composer.lock",
+		"conan.lock",
 		"Gemfile.lock",
 		"go.mod",
 		"gradle.lockfile",