diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 5b6716525cf65c2bed201516d35742d34969a3f4..c89cd097d2cd384f1f73bb608db3050c6cd60116 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -1492,6 +1492,24 @@ A use case for the latter is if you are a Renovate bot admin and wish to provide
 If `false` (default), it means that defining `config.npmrc` will result in any `.npmrc` file in the repo being overridden and its values ignored.
 If configured to `true`, it means that any `.npmrc` file in the repo will have `config.npmrc` prepended to it before running `npm`.
 
+## osvVulnerabilityAlerts
+
+Renovate integrates with [OSV](https://osv.dev/), an open-source vulnerability database, to check if extracted dependencies have known vulnerabilities.
+Set `osvVulnerabilityAlerts` to `true` to get pull requests with vulnerability fixes (once they are available).
+
+You will only get OSV-based vulnerability alerts for _direct_ dependencies.
+Renovate only queries the OSV database for dependencies that use one of these datasources:
+
+- [`crate`](https://docs.renovatebot.com/modules/datasource/crate/)
+- [`go`](https://docs.renovatebot.com/modules/datasource/go/)
+- [`hex`](https://docs.renovatebot.com/modules/datasource/hex/)
+- [`maven`](https://docs.renovatebot.com/modules/datasource/maven/)
+- [`npm`](https://docs.renovatebot.com/modules/datasource/npm/)
+- [`nuget`](https://docs.renovatebot.com/modules/datasource/nuget/)
+- [`packagist`](https://docs.renovatebot.com/modules/datasource/packagist/)
+- [`pypi`](https://docs.renovatebot.com/modules/datasource/pypi/)
+- [`rubygems`](https://docs.renovatebot.com/modules/datasource/rubygems/)
+
 ## packageRules
 
 `packageRules` is a powerful feature that lets you apply rules to individual packages or to groups of packages using regex pattern matching.
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index 3fbac96d6a37a1d259890437e37e236622a6d1d4..898292ff8089eac64e2b591fd76cbabfd8e8bc88 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -1670,6 +1670,14 @@ const options: RenovateOptions[] = [
     env: false,
     supportedPlatforms: ['github'],
   },
+  {
+    name: 'osvVulnerabilityAlerts',
+    description: 'Use vulnerability alerts from `osv.dev`.',
+    type: 'boolean',
+    default: false,
+    experimental: true,
+    experimentalIssues: [6562],
+  },
   {
     name: 'pruneBranchAfterAutomerge',
     description: 'Set to `true` to enable branch pruning after automerging.',
diff --git a/lib/config/types.ts b/lib/config/types.ts
index 5e67b80df5683b1489b52d5bb31e8da131a9f02d..3c2c8ef3757484a2f308d0bf7c537ef7105f97c6 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -243,6 +243,7 @@ export interface RenovateConfig
 
   warnings?: ValidationMessage[];
   vulnerabilityAlerts?: RenovateSharedConfig;
+  osvVulnerabilityAlerts?: boolean;
   regexManagers?: RegExManager[];
 
   fetchReleaseNotes?: boolean;
diff --git a/lib/workers/repository/process/extract-update.spec.ts b/lib/workers/repository/process/extract-update.spec.ts
index 42380976007b7e4ba7cb69917bb47a5a536e87f5..08f8d09343d1df16cb10ca464207d810590ba7b9 100644
--- a/lib/workers/repository/process/extract-update.spec.ts
+++ b/lib/workers/repository/process/extract-update.spec.ts
@@ -7,9 +7,21 @@ import { generateFingerprintConfig } from '../extract/extract-fingerprint-config
 import * as _branchify from '../updates/branchify';
 import { extract, isCacheExtractValid, lookup, update } from './extract-update';
 
+const createVulnerabilitiesMock = jest.fn();
+
 jest.mock('./write');
 jest.mock('./sort');
 jest.mock('./fetch');
+jest.mock('./vulnerabilities', () => {
+  return {
+    __esModule: true,
+    Vulnerabilities: class {
+      static create() {
+        return createVulnerabilitiesMock();
+      }
+    },
+  };
+});
 jest.mock('../updates/branchify');
 jest.mock('../extract');
 jest.mock('../../../util/cache/repository');
@@ -18,7 +30,7 @@ jest.mock('../../../util/git');
 const branchify = mocked(_branchify);
 const repositoryCache = mocked(_repositoryCache);
 
-branchify.branchifyUpgrades.mockResolvedValueOnce({
+branchify.branchifyUpgrades.mockResolvedValue({
   branches: [
     {
       manager: 'some-manager',
@@ -97,6 +109,42 @@ describe('workers/repository/process/extract-update', () => {
       const res = await extract(config);
       expect(res).toEqual(packageFiles);
     });
+
+    it('fetches vulnerabilities', async () => {
+      const config = {
+        repoIsOnboarded: true,
+        suppressNotifications: ['deprecationWarningIssues'],
+        osvVulnerabilityAlerts: true,
+      };
+      const fetchVulnerabilitiesMock = jest.fn();
+      createVulnerabilitiesMock.mockResolvedValueOnce({
+        fetchVulnerabilities: fetchVulnerabilitiesMock,
+      });
+      repositoryCache.getCache.mockReturnValueOnce({ scan: {} });
+      git.checkoutBranch.mockResolvedValueOnce('123test');
+
+      const packageFiles = await extract(config);
+      await lookup(config, packageFiles);
+
+      expect(createVulnerabilitiesMock).toHaveBeenCalledOnce();
+      expect(fetchVulnerabilitiesMock).toHaveBeenCalledOnce();
+    });
+
+    it('handles exception when fetching vulnerabilities', async () => {
+      const config = {
+        repoIsOnboarded: true,
+        suppressNotifications: ['deprecationWarningIssues'],
+        osvVulnerabilityAlerts: true,
+      };
+      createVulnerabilitiesMock.mockRejectedValueOnce(new Error());
+      repositoryCache.getCache.mockReturnValueOnce({ scan: {} });
+      git.checkoutBranch.mockResolvedValueOnce('123test');
+
+      const packageFiles = await extract(config);
+      await lookup(config, packageFiles);
+
+      expect(createVulnerabilitiesMock).toHaveBeenCalledOnce();
+    });
   });
 
   describe('isCacheExtractValid()', () => {
diff --git a/lib/workers/repository/process/extract-update.ts b/lib/workers/repository/process/extract-update.ts
index 763aadd539ff2484bb43a4497b9fd1c0fed884cd..e806b545281090fec8348a4df3af0f31c9d590b0 100644
--- a/lib/workers/repository/process/extract-update.ts
+++ b/lib/workers/repository/process/extract-update.ts
@@ -15,6 +15,7 @@ import { branchifyUpgrades } from '../updates/branchify';
 import { raiseDeprecationWarnings } from './deprecated';
 import { fetchUpdates } from './fetch';
 import { sortBranches } from './sort';
+import { Vulnerabilities } from './vulnerabilities';
 import { WriteUpdateResult, writeUpdates } from './write';
 
 export interface ExtractResult {
@@ -166,10 +167,25 @@ export async function extract(
   return packageFiles;
 }
 
+async function fetchVulnerabilities(
+  config: RenovateConfig,
+  packageFiles: Record<string, PackageFile[]>
+): Promise<void> {
+  if (config.osvVulnerabilityAlerts) {
+    try {
+      const vulnerabilities = await Vulnerabilities.create();
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+    } catch (err) {
+      logger.warn({ err }, 'Unable to read vulnerability information');
+    }
+  }
+}
+
 export async function lookup(
   config: RenovateConfig,
   packageFiles: Record<string, PackageFile[]>
 ): Promise<ExtractResult> {
+  await fetchVulnerabilities(config, packageFiles);
   await fetchUpdates(config, packageFiles);
   await raiseDeprecationWarnings(config, packageFiles);
   const { branches, branchList } = await branchifyUpgrades(
diff --git a/lib/workers/repository/process/lookup/index.ts b/lib/workers/repository/process/lookup/index.ts
index 9e55551e00923157df056fa47a9d60eaa3598170..49763a3336b82930ee420bd330e520345c6dc24c 100644
--- a/lib/workers/repository/process/lookup/index.ts
+++ b/lib/workers/repository/process/lookup/index.ts
@@ -252,7 +252,7 @@ export async function lookupUpdates(
           // Leave only compatible versions
           unconstrainedValue || versioning.isCompatible(v.version, currentValue)
       );
-      if (isVulnerabilityAlert) {
+      if (isVulnerabilityAlert && !config.osvVulnerabilityAlerts) {
         filteredReleases = filteredReleases.slice(0, 1);
       }
       const buckets: Record<string, [Release]> = {};
diff --git a/lib/workers/repository/process/vulnerabilities.spec.ts b/lib/workers/repository/process/vulnerabilities.spec.ts
index 775901e43270a91127ec6125f1a2d9cb88dc1414..8a03529383d8e78d172981b441aa88b4d66f6f12 100644
--- a/lib/workers/repository/process/vulnerabilities.spec.ts
+++ b/lib/workers/repository/process/vulnerabilities.spec.ts
@@ -1,6 +1,7 @@
-import type { Ecosystem, OsvOffline } from '@renovatebot/osv-offline';
+import type { Osv, OsvOffline } from '@renovatebot/osv-offline';
+import { codeBlock } from 'common-tags';
 import { mockFn } from 'jest-mock-extended';
-import { getConfig } from '../../../../test/util';
+import { RenovateConfig, getConfig, logger } from '../../../../test/util';
 import type { PackageFile } from '../../../modules/manager/types';
 import { Vulnerabilities } from './vulnerabilities';
 
@@ -33,37 +34,730 @@ describe('workers/repository/process/vulnerabilities', () => {
   });
 
   describe('fetchVulnerabilities()', () => {
-    const config = getConfig();
-    const packageFiles: Record<string, PackageFile[]> = {
-      npm: [{ deps: [{ depName: 'lodash' }] }],
-    };
+    let config: RenovateConfig;
     let vulnerabilities: Vulnerabilities;
+    const lodashVulnerability: Osv.Vulnerability = {
+      id: 'GHSA-x5rq-j2xg-h7qm',
+      modified: '',
+      affected: [
+        {
+          ranges: [
+            {
+              type: 'SEMVER',
+              events: [{ introduced: '0.0.0' }, { fixed: '4.17.11' }],
+            },
+          ],
+          package: { name: 'lodash', ecosystem: 'npm' },
+        },
+      ],
+      references: [
+        {
+          type: 'ADVISORY',
+          url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-1010266',
+        },
+      ],
+    };
 
     beforeAll(async () => {
       createMock.mockResolvedValue({
-        getVulnerabilities: (ecosystem: Ecosystem, packageName: string) =>
-          getVulnerabilitiesMock(ecosystem, packageName),
+        getVulnerabilities: getVulnerabilitiesMock,
       });
       vulnerabilities = await Vulnerabilities.create();
     });
 
-    it('works', async () => {
-      getVulnerabilitiesMock.mockResolvedValue([
+    beforeEach(() => {
+      config = getConfig();
+      config.packageRules = [];
+    });
+
+    it('unsupported datasource', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        dockerfile: [{ deps: [{ depName: 'node', datasource: 'docker' }] }],
+      };
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(logger.logger.trace).toHaveBeenCalledWith(
+        'Cannot map datasource docker to OSV ecosystem'
+      );
+    });
+
+    it('package found but no vulnerabilities', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        npm: [{ deps: [{ depName: 'lodash', datasource: 'npm' }] }],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(logger.logger.trace).toHaveBeenCalledWith(
+        'No vulnerabilities found in OSV database for lodash'
+      );
+    });
+
+    it('vulnerability without affected field', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        npm: [
+          {
+            deps: [
+              { depName: 'lodash', currentValue: '4.17.11', datasource: 'npm' },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([
+        {
+          id: 'GHSA-p6mc-m468-83gw',
+          modified: '',
+        },
+      ]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(config.packageRules).toHaveLength(0);
+    });
+
+    it('invalid dep version', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        npm: [
+          {
+            deps: [
+              {
+                depName: 'lodash',
+                currentValue: '#4.17.11',
+                datasource: 'npm',
+              },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([lodashVulnerability]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(logger.logger.debug).toHaveBeenCalledWith(
+        'Skipping vulnerability lookup for package lodash due to unsupported version #4.17.11'
+      );
+    });
+
+    it('exception due to invalid version upon comparison', async () => {
+      const err = new TypeError('Invalid Version: ^1.1.0');
+      const packageFiles: Record<string, PackageFile[]> = {
+        npm: [
+          {
+            deps: [
+              {
+                depName: 'lodash',
+                currentValue: '4.17.11',
+                datasource: 'npm',
+              },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([
+        {
+          id: 'GHSA-xxxx-yyyy-zzzz',
+          modified: '',
+          affected: [
+            {
+              package: {
+                name: 'lodash',
+                ecosystem: 'npm',
+                purl: 'pkg:npm/lodash',
+              },
+              ranges: [
+                {
+                  type: 'SEMVER',
+                  events: [{ introduced: '^0' }, { fixed: '^1.1.0' }],
+                },
+              ],
+            },
+          ],
+        },
+      ]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(logger.logger.debug).toHaveBeenCalledWith(
+        { err },
+        'Error fetching vulnerability information for lodash'
+      );
+    });
+
+    it('no version or range affected', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        npm: [
+          {
+            deps: [
+              { depName: 'fake', currentValue: '4.17.11', datasource: 'npm' },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([
+        {
+          id: 'GHSA-xxxx-yyyy-zzzz',
+          modified: '',
+          affected: [
+            {
+              package: { name: 'fake', ecosystem: 'npm', purl: 'pkg:npm/fake' },
+            },
+          ],
+        },
+      ]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(config.packageRules).toHaveLength(0);
+    });
+
+    it('no fixed version available', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        npm: [
+          {
+            deps: [
+              { depName: 'fake', currentValue: '4.17.11', datasource: 'npm' },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([
+        {
+          id: 'GHSA-xxxx-yyyy-zzzz',
+          modified: '',
+          affected: [
+            {
+              package: { name: 'fake', ecosystem: 'npm', purl: 'pkg:npm/fake' },
+              versions: ['4.17.11'],
+            },
+          ],
+        },
+      ]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(logger.logger.info).toHaveBeenCalledWith(
+        'No fixed version available for vulnerability GHSA-xxxx-yyyy-zzzz in fake 4.17.11'
+      );
+    });
+
+    it('does not accidentally downgrade versions due to fixed version for other range', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        npm: [
+          {
+            deps: [
+              { depName: 'fake', currentValue: '1.5.1', datasource: 'npm' },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([
+        {
+          id: 'GHSA-xxxx-yyyy-zzzz',
+          modified: '',
+          affected: [
+            {
+              ranges: [
+                {
+                  type: 'SEMVER',
+                  events: [{ introduced: '0' }, { fixed: '1.1.0' }],
+                },
+                {
+                  type: 'SEMVER',
+                  events: [{ introduced: '1.3.0' }],
+                },
+              ],
+              package: { name: 'fake', ecosystem: 'npm' },
+            },
+          ],
+        },
+      ]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(logger.logger.info).toHaveBeenCalledWith(
+        'No fixed version available for vulnerability GHSA-xxxx-yyyy-zzzz in fake 1.5.1'
+      );
+    });
+
+    it('vulnerability with multiple unsorted events', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        gomod: [
+          {
+            deps: [
+              { depName: 'stdlib', currentValue: '1.7.5', datasource: 'go' },
+            ],
+          },
+        ],
+      };
+
+      getVulnerabilitiesMock.mockResolvedValueOnce([
         {
-          id: 'ABCD',
+          id: 'GO-2022-0187',
           modified: '',
+          aliases: ['CVE-2017-8932'],
           affected: [
             {
-              ranges: [{ type: 'SEMVER', events: [{ fixed: '1.2.3' }] }],
+              package: {
+                name: 'stdlib',
+                ecosystem: 'Go',
+                purl: 'pkg:golang/stdlib',
+              },
+              ranges: [
+                {
+                  type: 'SEMVER',
+                  events: [
+                    { introduced: '1.6.0' },
+                    { fixed: '1.8.5' },
+                    { introduced: '1.8.3' },
+                    { fixed: '1.7.6' },
+                  ],
+                },
+              ],
+            },
+          ],
+        },
+      ]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(logger.logger.debug).toHaveBeenCalledWith(
+        'Vulnerability GO-2022-0187 affects stdlib 1.7.5'
+      );
+      expect(logger.logger.debug).toHaveBeenCalledWith(
+        'Setting allowed version 1.7.6 to fix vulnerability GO-2022-0187 in stdlib 1.7.5'
+      );
+      expect(config.packageRules).toHaveLength(1);
+      expect(config.packageRules).toMatchObject([
+        {
+          matchDatasources: ['go'],
+          matchPackageNames: ['stdlib'],
+          matchCurrentVersion: '1.7.5',
+          allowedVersions: '1.7.6',
+          isVulnerabilityAlert: true,
+        },
+      ]);
+    });
+
+    it('vulnerability with multiple affected entries and version ranges', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        poetry: [
+          {
+            deps: [
+              { depName: 'django', currentValue: '3.2', datasource: 'pypi' },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([
+        {
+          id: 'GHSA-qrw5-5h28-modded',
+          modified: '',
+          affected: [
+            {
+              package: {
+                name: 'django',
+                ecosystem: 'PyPI',
+                purl: 'pkg:pypi/django',
+              },
+              ranges: [
+                {
+                  type: 'GIT',
+                  repo: 'https://github.com/django/django',
+                  events: [
+                    { introduced: '0' },
+                    { fixed: '5b6b257fa7ec37ff27965358800c67e2dd11c924' },
+                  ],
+                },
+                {
+                  type: 'ECOSYSTEM',
+                  events: [{ introduced: '3.2' }, { fixed: '3.2.16' }],
+                },
+              ],
+              versions: ['3.2.1', '3.2.10', '3.2.9'],
+            },
+            {
+              package: {
+                name: 'django',
+                ecosystem: 'PyPI',
+                purl: 'pkg:pypi/django',
+              },
+              ranges: [
+                {
+                  type: 'ECOSYSTEM',
+                  events: [{ introduced: '4.0' }, { fixed: '4.0.8' }],
+                },
+              ],
+              versions: ['4.0', '4.0.1', '4.0.6', '4.0.7'],
+            },
+          ],
+        },
+      ]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(config.packageRules).toHaveLength(1);
+      expect(config.packageRules).toMatchObject([
+        {
+          matchDatasources: ['pypi'],
+          matchPackageNames: ['django'],
+          matchCurrentVersion: '3.2',
+          allowedVersions: '==3.2.16',
+          isVulnerabilityAlert: true,
+        },
+      ]);
+    });
+
+    it('filters not applicable vulnerability', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        npm: [
+          {
+            deps: [
+              { depName: 'lodash', currentValue: '4.17.11', datasource: 'npm' },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([lodashVulnerability]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(config.packageRules).toHaveLength(0);
+    });
+
+    it('returns a single packageRule for regex manager', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        regex: [
+          {
+            deps: [
+              {
+                depName: 'tiny_http',
+                currentValue: '0.1.2',
+                datasource: 'crate',
+              },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([
+        {
+          id: 'RUSTSEC-2020-0031',
+          summary:
+            'HTTP Request smuggling through malformed Transfer Encoding headers',
+          details:
+            'HTTP pipelining issues and request smuggling attacks are possible due to incorrect Transfer encoding header parsing.\n\nIt is possible conduct HTTP request smuggling attacks (CL:TE/TE:TE) by sending invalid Transfer Encoding headers.\n\nBy manipulating the HTTP response the attacker could poison a web-cache, perform an XSS attack, or obtain sensitive information from requests other than their own.',
+          aliases: ['CVE-2020-35884', 'SOME-1234-5678'],
+          modified: '',
+          affected: [
+            {
+              package: {
+                name: 'tiny_http',
+                ecosystem: 'crates.io',
+                purl: 'pkg:cargo/tiny_http',
+              },
+              ranges: [
+                {
+                  type: 'SEMVER',
+                  events: [
+                    { introduced: '0' },
+                    { fixed: '0.6.3' },
+                    { introduced: '0.7.0-0' },
+                    { fixed: '0.8.0' },
+                  ],
+                },
+              ],
+            },
+          ],
+          severity: [
+            {
+              type: 'CVSS_V2',
+              score: 'AV:N/AC:L/Au:N/C:P/I:P/A:N',
+            },
+            {
+              type: 'CVSS_V3',
+              score: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N',
+            },
+          ],
+        },
+      ]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+
+      expect(config.packageRules).toHaveLength(1);
+      expect(config.packageRules).toMatchObject([
+        {
+          matchDatasources: ['crate'],
+          matchPackageNames: ['tiny_http'],
+          matchCurrentVersion: '0.1.2',
+          allowedVersions: '0.6.3',
+          isVulnerabilityAlert: true,
+          prBodyNotes: [
+            '\n\n' +
+              codeBlock`
+              ---
+
+              ### HTTP Request smuggling through malformed Transfer Encoding headers
+              [CVE-2020-35884](https://nvd.nist.gov/vuln/detail/CVE-2020-35884) / [RUSTSEC-2020-0031](https://rustsec.org/advisories/RUSTSEC-2020-0031.html) / SOME-1234-5678
+
+              <details>
+              <summary>More information</summary>
+
+              ### Details
+              HTTP pipelining issues and request smuggling attacks are possible due to incorrect Transfer encoding header parsing.
+
+              It is possible conduct HTTP request smuggling attacks (CL:TE/TE:TE) by sending invalid Transfer Encoding headers.
+
+              By manipulating the HTTP response the attacker could poison a web-cache, perform an XSS attack, or obtain sensitive information from requests other than their own.
+
+              ### Severity
+              - Score: 6.5 / 10 (Medium)
+              - Vector: \`CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N\`
+
+              ### References
+              No references.
+
+              This data is provided by [OSV](https://osv.dev/vulnerability/RUSTSEC-2020-0031) and the [Rust Advisory Database](https://github.com/RustSec/advisory-db) ([CC0 1.0](https://github.com/rustsec/advisory-db/blob/main/LICENSE.txt)).
+              </details>
+            `,
+          ],
+        },
+      ]);
+    });
+
+    it('returns multiple packageRules for different vulnerabilities', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        npm: [
+          {
+            deps: [
+              { depName: 'lodash', currentValue: '4.17.10', datasource: 'npm' },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([
+        lodashVulnerability,
+        {
+          id: 'GHSA-p6mc-m468-83gw',
+          modified: '',
+          affected: [
+            {
+              ranges: [
+                {
+                  type: 'SEMVER',
+                  events: [{ introduced: '0' }, { fixed: '4.17.20' }],
+                },
+              ],
               package: { name: 'lodash', ecosystem: 'npm' },
             },
           ],
+          severity: [
+            {
+              type: 'CVSS_V3',
+              score: 'CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:H',
+            },
+          ],
         },
       ]);
 
       await vulnerabilities.fetchVulnerabilities(config, packageFiles);
 
+      expect(config.packageRules).toHaveLength(2);
+      expect(config.packageRules).toMatchObject([
+        {
+          matchDatasources: ['npm'],
+          matchPackageNames: ['lodash'],
+          matchCurrentVersion: '4.17.10',
+          allowedVersions: '4.17.11',
+          isVulnerabilityAlert: true,
+        },
+        {
+          matchDatasources: ['npm'],
+          matchPackageNames: ['lodash'],
+          matchCurrentVersion: '4.17.10',
+          allowedVersions: '4.17.20',
+          isVulnerabilityAlert: true,
+        },
+      ]);
+    });
+
+    it('filters not applicable vulnerability based on last_affected version', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        poetry: [
+          {
+            deps: [
+              { depName: 'quokka', currentValue: '1.2.3', datasource: 'pypi' },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([
+        {
+          id: 'GHSA-xxxx-yyyy-zzzz',
+          modified: '',
+          affected: [
+            {
+              package: {
+                name: 'quokka',
+                ecosystem: 'PyPI',
+                purl: 'pkg:pypi/quokka',
+              },
+              ranges: [
+                {
+                  type: 'ECOSYSTEM',
+                  events: [{ introduced: '0' }, { last_affected: '0.4.0' }],
+                },
+              ],
+            },
+          ],
+        },
+      ]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(logger.logger.debug).not.toHaveBeenCalledWith(
+        'OSV advisory GHSA-xxxx-yyyy-zzzz lists quokka 1.2.3 as vulnerable'
+      );
+      expect(config.packageRules).toHaveLength(0);
+    });
+
+    it('returns packageRule based on last_affected version', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        npm: [
+          {
+            deps: [
+              { depName: 'lodash', currentValue: '0.5.0', datasource: 'npm' },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([
+        {
+          id: 'GHSA-xxxx-yyyy-zzzz',
+          modified: '',
+          affected: [
+            {
+              package: {
+                name: 'lodash',
+                ecosystem: 'npm',
+                purl: 'pkg:npm/lodash',
+              },
+              ranges: [
+                {
+                  type: 'SEMVER',
+                  events: [{ introduced: '0' }, { last_affected: '0.2.0' }],
+                },
+                {
+                  type: 'SEMVER',
+                  events: [{ introduced: '0.4.0' }, { last_affected: '0.8.0' }],
+                },
+              ],
+            },
+          ],
+        },
+      ]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+      expect(config.packageRules).toHaveLength(1);
+      expect(config.packageRules).toMatchObject([
+        {
+          matchDatasources: ['npm'],
+          matchPackageNames: ['lodash'],
+          matchCurrentVersion: '0.5.0',
+          allowedVersions: '> 0.8.0',
+          isVulnerabilityAlert: true,
+          prBodyNotes: [
+            '\n\n' +
+              codeBlock`
+              ---
+
+              ### [GHSA-xxxx-yyyy-zzzz](https://github.com/advisories/GHSA-xxxx-yyyy-zzzz)
+
+              <details>
+              <summary>More information</summary>
+
+              ### Details
+              No details.
+
+              ### Severity
+              Unknown severity.
+
+              ### References
+              No references.
+
+              This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-xxxx-yyyy-zzzz) and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)).
+              </details>
+            `,
+          ],
+        },
+      ]);
+    });
+
+    it('handles invalid CVSS scores gracefully', async () => {
+      const packageFiles: Record<string, PackageFile[]> = {
+        poetry: [
+          {
+            deps: [
+              {
+                depName: 'django-mfa2',
+                currentValue: '2.5.0',
+                datasource: 'pypi',
+              },
+            ],
+          },
+        ],
+      };
+      getVulnerabilitiesMock.mockResolvedValueOnce([
+        {
+          id: 'PYSEC-2022-303',
+          modified: '',
+          affected: [
+            {
+              ranges: [
+                {
+                  type: 'ECOSYSTEM',
+                  events: [{ introduced: '0' }, { fixed: '2.5.1' }],
+                },
+              ],
+              package: { name: 'django-mfa2', ecosystem: 'PyPI' },
+            },
+          ],
+          severity: [
+            {
+              type: 'CVSS_V3',
+              score: 'some-invalid-score',
+            },
+          ],
+        },
+      ]);
+
+      await vulnerabilities.fetchVulnerabilities(config, packageFiles);
+
+      expect(logger.logger.debug).toHaveBeenCalledWith(
+        'Error processing CVSS vector some-invalid-score'
+      );
       expect(config.packageRules).toHaveLength(1);
+      expect(config.packageRules).toMatchObject([
+        {
+          matchDatasources: ['pypi'],
+          matchPackageNames: ['django-mfa2'],
+          matchCurrentVersion: '2.5.0',
+          allowedVersions: '==2.5.1',
+          isVulnerabilityAlert: true,
+          prBodyNotes: [
+            '\n\n' +
+              codeBlock`
+              ---
+
+              ### PYSEC-2022-303
+
+              <details>
+              <summary>More information</summary>
+
+              ### Details
+              No details.
+
+              ### Severity
+              - Score: Unknown
+              - Vector: \`some-invalid-score\`
+
+              ### References
+              No references.
+
+              This data is provided by [OSV](https://osv.dev/vulnerability/PYSEC-2022-303) and the [PyPI Advisory Database](https://github.com/pypa/advisory-database) ([CC-BY 4.0](https://github.com/pypa/advisory-database/blob/main/LICENSE)).
+              </details>
+            `,
+          ],
+        },
+      ]);
     });
   });
 });
diff --git a/lib/workers/repository/process/vulnerabilities.ts b/lib/workers/repository/process/vulnerabilities.ts
index 019895789b3d59033ef674ece06bb74fbf8f0a84..3ab53afe7af2f3a36b562a2f6c59ad9bc122d30e 100644
--- a/lib/workers/repository/process/vulnerabilities.ts
+++ b/lib/workers/repository/process/vulnerabilities.ts
@@ -1,36 +1,40 @@
 // TODO #7154
 import { Ecosystem, Osv, OsvOffline } from '@renovatebot/osv-offline';
+import is from '@sindresorhus/is';
+import type { CvssScore } from 'vuln-vects';
+import { parseCvssVector } from 'vuln-vects';
 import { getManagerConfig, mergeChildConfig } from '../../../config';
 import type { PackageRule, RenovateConfig } from '../../../config/types';
 import { logger } from '../../../logger';
+import { getDefaultVersioning } from '../../../modules/datasource';
 import type {
   PackageDependency,
   PackageFile,
 } from '../../../modules/manager/types';
+import {
+  VersioningApi,
+  get as getVersioning,
+} from '../../../modules/versioning';
+import { sanitizeMarkdown } from '../../../util/markdown';
 import * as p from '../../../util/promises';
+import { regEx } from '../../../util/regex';
 
 export class Vulnerabilities {
   private osvOffline: OsvOffline | undefined;
 
-  private static readonly managerEcosystemMap: Record<
+  private static readonly datasourceEcosystemMap: Record<
     string,
     Ecosystem | undefined
   > = {
-    bundler: 'RubyGems',
-    cargo: 'crates.io',
-    gomod: 'Go',
-    gradle: 'Maven',
+    crate: 'crates.io',
+    go: 'Go',
+    hex: 'Hex',
     maven: 'Maven',
-    meteor: 'npm',
     npm: 'npm',
     nuget: 'NuGet',
-    'pip-compile': 'PyPI',
-    pip_requirements: 'PyPI',
-    pip_setup: 'PyPI',
-    pipenv: 'PyPI',
-    poetry: 'PyPI',
-    'setup-cfg': 'PyPI',
-    sbt: 'Maven',
+    packagist: 'Packagist',
+    pypi: 'PyPI',
+    rubygems: 'RubyGems',
   };
 
   // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -50,9 +54,7 @@ export class Vulnerabilities {
     config: RenovateConfig,
     packageFiles: Record<string, PackageFile[]>
   ): Promise<void> {
-    const managers = Object.keys(packageFiles).filter(
-      (manager) => Vulnerabilities.managerEcosystemMap[manager] !== undefined
-    );
+    const managers = Object.keys(packageFiles);
     const allManagerJobs = managers.map((manager) =>
       this.fetchManagerVulnerabilities(config, packageFiles, manager)
     );
@@ -67,7 +69,11 @@ export class Vulnerabilities {
     const managerConfig = getManagerConfig(config, manager);
     const queue = packageFiles[manager].map(
       (pFile) => (): Promise<void> =>
-        this.fetchManagerPackagerFileUpdates(config, managerConfig, pFile)
+        this.fetchManagerPackageFileVulnerabilities(
+          config,
+          managerConfig,
+          pFile
+        )
     );
     logger.trace(
       { manager, queueLength: queue.length },
@@ -77,7 +83,7 @@ export class Vulnerabilities {
     logger.trace({ manager }, 'fetchManagerUpdates finished');
   }
 
-  private async fetchManagerPackagerFileUpdates(
+  private async fetchManagerPackageFileVulnerabilities(
     config: RenovateConfig,
     managerConfig: RenovateConfig,
     pFile: PackageFile
@@ -91,51 +97,393 @@ export class Vulnerabilities {
     );
     logger.trace(
       { manager, packageFile, queueLength: queue.length },
-      'fetchManagerPackagerFileUpdates starting with concurrency'
+      'fetchManagerPackageFileVulnerabilities starting with concurrency'
     );
 
     config.packageRules?.push(...(await p.all(queue)).flat());
-    logger.trace({ packageFile }, 'fetchManagerPackagerFileUpdates finished');
+    logger.trace(
+      { packageFile },
+      'fetchManagerPackageFileVulnerabilities finished'
+    );
   }
 
   private async fetchDependencyVulnerabilities(
     packageFileConfig: RenovateConfig & PackageFile,
-    packageDependency: PackageDependency
+    dep: PackageDependency
   ): Promise<PackageRule[]> {
-    const ecosystem =
-      Vulnerabilities.managerEcosystemMap[packageFileConfig.manager!];
+    const ecosystem = Vulnerabilities.datasourceEcosystemMap[dep.datasource!];
+    if (!ecosystem) {
+      logger.trace(`Cannot map datasource ${dep.datasource!} to OSV ecosystem`);
+      return [];
+    }
+
+    let packageName = dep.packageName ?? dep.depName!;
+    if (ecosystem === 'PyPI') {
+      // https://peps.python.org/pep-0503/#normalized-names
+      packageName = packageName.toLowerCase().replace(regEx(/[_.-]+/g), '-');
+    }
+
+    const packageRules: PackageRule[] = [];
+    try {
+      const vulnerabilities = await this.osvOffline?.getVulnerabilities(
+        ecosystem,
+        packageName
+      );
+      if (
+        is.nullOrUndefined(vulnerabilities) ||
+        is.emptyArray(vulnerabilities)
+      ) {
+        logger.trace(
+          `No vulnerabilities found in OSV database for ${packageName}`
+        );
+        return [];
+      }
+
+      const depVersion =
+        dep.lockedVersion ?? dep.currentVersion ?? dep.currentValue!;
+
+      const versioning = dep.versioning ?? getDefaultVersioning(dep.datasource);
+      const versioningApi = getVersioning(versioning);
+
+      if (!versioningApi.isVersion(depVersion)) {
+        logger.debug(
+          `Skipping vulnerability lookup for package ${packageName} due to unsupported version ${depVersion}`
+        );
+        return [];
+      }
+
+      for (const vulnerability of vulnerabilities) {
+        for (const affected of vulnerability.affected ?? []) {
+          const isVulnerable = this.isPackageVulnerable(
+            ecosystem,
+            packageName,
+            depVersion,
+            affected,
+            versioningApi
+          );
+          if (!isVulnerable) {
+            continue;
+          }
+
+          logger.debug(
+            `Vulnerability ${vulnerability.id} affects ${packageName} ${depVersion}`
+          );
+          const fixedVersion = this.getFixedVersion(
+            ecosystem,
+            depVersion,
+            affected,
+            versioningApi
+          );
+          if (is.nullOrUndefined(fixedVersion)) {
+            logger.info(
+              `No fixed version available for vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`
+            );
+            continue;
+          }
+
+          logger.debug(
+            `Setting allowed version ${fixedVersion} to fix vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`
+          );
+          const rule = this.convertToPackageRule(
+            packageFileConfig,
+            dep,
+            packageName,
+            depVersion,
+            fixedVersion,
+            vulnerability
+          );
+          packageRules.push(rule);
+        }
+      }
+
+      this.sortByFixedVersion(packageRules, versioningApi);
+    } catch (err) {
+      logger.debug(
+        { err },
+        `Error fetching vulnerability information for ${packageName}`
+      );
+    }
+
+    return packageRules;
+  }
+
+  private sortByFixedVersion(
+    packageRules: PackageRule[],
+    versioningApi: VersioningApi
+  ): void {
+    const versionsCleaned: Record<string, string> = {};
+    for (const rule of packageRules) {
+      const version = rule.allowedVersions as string;
+      versionsCleaned[version] = version.replace(regEx(/[=> ]+/g), '');
+    }
 
-    const vulnerabilities = await this.osvOffline?.getVulnerabilities(
-      ecosystem!,
-      packageDependency.depName!
+    packageRules.sort((a, b) =>
+      versioningApi.sortVersions(
+        versionsCleaned[a.allowedVersions as string],
+        versionsCleaned[b.allowedVersions as string]
+      )
     );
-    return this.convertToPackageRule(
-      vulnerabilities ?? [],
-      packageDependency.depName!,
-      ecosystem!
+  }
+
+  // https://ossf.github.io/osv-schema/#affectedrangesevents-fields
+  private sortEvents(
+    events: Osv.Event[],
+    versioningApi: VersioningApi
+  ): Osv.Event[] {
+    const sortedCopy: Osv.Event[] = [];
+    let zeroEvent: Osv.Event | null = null;
+
+    for (const event of events) {
+      if (event.introduced === '0') {
+        zeroEvent = event;
+        continue;
+      }
+      sortedCopy.push(event);
+    }
+
+    sortedCopy.sort((a, b) =>
+      // no pre-processing, as there are only very few values to sort
+      versioningApi.sortVersions(Object.values(a)[0], Object.values(b)[0])
+    );
+
+    if (zeroEvent) {
+      sortedCopy.unshift(zeroEvent);
+    }
+
+    return sortedCopy;
+  }
+
+  private isPackageAffected(
+    ecosystem: Ecosystem,
+    packageName: string,
+    affected: Osv.Affected
+  ): boolean {
+    return (
+      affected.package?.name === packageName &&
+      affected.package?.ecosystem === ecosystem
+    );
+  }
+
+  private includedInVersions(
+    depVersion: string,
+    affected: Osv.Affected
+  ): boolean {
+    return !!affected.versions?.includes(depVersion);
+  }
+
+  private includedInRanges(
+    depVersion: string,
+    affected: Osv.Affected,
+    versioningApi: VersioningApi
+  ): boolean {
+    for (const range of affected.ranges ?? []) {
+      if (range.type === 'GIT') {
+        continue;
+      }
+
+      let vulnerable = false;
+      for (const event of this.sortEvents(range.events, versioningApi)) {
+        if (
+          is.nonEmptyString(event.introduced) &&
+          (event.introduced === '0' ||
+            this.isVersionGtOrEq(depVersion, event.introduced, versioningApi))
+        ) {
+          vulnerable = true;
+        } else if (
+          is.nonEmptyString(event.fixed) &&
+          this.isVersionGtOrEq(depVersion, event.fixed, versioningApi)
+        ) {
+          vulnerable = false;
+        } else if (
+          is.nonEmptyString(event.last_affected) &&
+          this.isVersionGt(depVersion, event.last_affected, versioningApi)
+        ) {
+          vulnerable = false;
+        }
+      }
+
+      if (vulnerable) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  // https://ossf.github.io/osv-schema/#evaluation
+  private isPackageVulnerable(
+    ecosystem: Ecosystem,
+    packageName: string,
+    depVersion: string,
+    affected: Osv.Affected,
+    versioningApi: VersioningApi
+  ): boolean {
+    return (
+      this.isPackageAffected(ecosystem, packageName, affected) &&
+      (this.includedInVersions(depVersion, affected) ||
+        this.includedInRanges(depVersion, affected, versioningApi))
+    );
+  }
+
+  private getFixedVersion(
+    ecosystem: Ecosystem,
+    depVersion: string,
+    affected: Osv.Affected,
+    versioningApi: VersioningApi
+  ): string | null {
+    const fixedVersions: string[] = [];
+    const lastAffectedVersions: string[] = [];
+
+    for (const range of affected.ranges ?? []) {
+      if (range.type === 'GIT') {
+        continue;
+      }
+
+      for (const event of range.events) {
+        if (is.nonEmptyString(event.fixed)) {
+          fixedVersions.push(event.fixed);
+        } else if (is.nonEmptyString(event.last_affected)) {
+          lastAffectedVersions.push(event.last_affected);
+        }
+      }
+    }
+
+    fixedVersions.sort((a, b) => versioningApi.sortVersions(a, b));
+    const fixedVersion = fixedVersions.find((version) =>
+      this.isVersionGt(version, depVersion, versioningApi)
+    );
+    if (fixedVersion) {
+      return ecosystem === 'PyPI' ? `==${fixedVersion}` : fixedVersion;
+    }
+
+    lastAffectedVersions.sort((a, b) => versioningApi.sortVersions(a, b));
+    const lastAffected = lastAffectedVersions.find((version) =>
+      this.isVersionGtOrEq(version, depVersion, versioningApi)
+    );
+    if (lastAffected) {
+      return `> ${lastAffected}`;
+    }
+
+    return null;
+  }
+
+  private isVersionGt(
+    version: string,
+    other: string,
+    versioningApi: VersioningApi
+  ): boolean {
+    return (
+      versioningApi.isVersion(version) &&
+      versioningApi.isVersion(other) &&
+      versioningApi.isGreaterThan(version, other)
+    );
+  }
+
+  private isVersionGtOrEq(
+    version: string,
+    other: string,
+    versioningApi: VersioningApi
+  ): boolean {
+    return (
+      versioningApi.isVersion(version) &&
+      versioningApi.isVersion(other) &&
+      (versioningApi.equals(version, other) ||
+        versioningApi.isGreaterThan(version, other))
     );
   }
 
   private convertToPackageRule(
-    vulnerabilities: Osv.Vulnerability[],
-    dependencyName: string,
-    ecosystem: Ecosystem
-  ): PackageRule[] {
-    return vulnerabilities
-      .flatMap((vulnerability) => vulnerability.affected)
-      .filter(
-        (vulnerability) =>
-          vulnerability?.package?.name === dependencyName &&
-          vulnerability?.package?.ecosystem === ecosystem
-      )
-      .map(
-        (affected): PackageRule => ({
-          matchPackageNames: [dependencyName],
-          allowedVersions: affected?.ranges?.[0].events.find(
-            (event) => event.fixed !== undefined
-          )!.fixed,
-          isVulnerabilityAlert: true,
+    packageFileConfig: RenovateConfig & PackageFile,
+    dep: PackageDependency,
+    packageName: string,
+    depVersion: string,
+    fixedVersion: string,
+    vulnerability: Osv.Vulnerability
+  ): PackageRule {
+    return {
+      matchDatasources: [dep.datasource!],
+      matchPackageNames: [packageName],
+      matchCurrentVersion: depVersion,
+      allowedVersions: fixedVersion,
+      isVulnerabilityAlert: true,
+      prBodyNotes: this.generatePrBodyNotes(vulnerability),
+      force: {
+        ...packageFileConfig.vulnerabilityAlerts,
+      },
+    };
+  }
+
+  private evaluateCvssVector(vector: string): [string, string] {
+    try {
+      const parsedCvss: CvssScore = parseCvssVector(vector);
+      const severityLevel =
+        parsedCvss.cvss3OverallSeverityText.charAt(0).toUpperCase() +
+        parsedCvss.cvss3OverallSeverityText.slice(1);
+
+      return [parsedCvss.baseScore.toFixed(1), severityLevel];
+    } catch (err) {
+      logger.debug(`Error processing CVSS vector ${vector}`);
+    }
+
+    return ['', ''];
+  }
+
+  private generatePrBodyNotes(vulnerability: Osv.Vulnerability): string[] {
+    let aliases = [vulnerability.id].concat(vulnerability.aliases ?? []).sort();
+    aliases = aliases.map((id) => {
+      if (id.startsWith('CVE-')) {
+        return `[${id}](https://nvd.nist.gov/vuln/detail/${id})`;
+      } else if (id.startsWith('GHSA-')) {
+        return `[${id}](https://github.com/advisories/${id})`;
+      } else if (id.startsWith('GO-')) {
+        return `[${id}](https://pkg.go.dev/vuln/${id})`;
+      } else if (id.startsWith('RUSTSEC-')) {
+        return `[${id}](https://rustsec.org/advisories/${id}.html)`;
+      }
+
+      return id;
+    });
+
+    let content = '\n\n---\n\n### ';
+    content += vulnerability.summary ? `${vulnerability.summary}\n` : '';
+    content += `${aliases.join(' / ')}\n`;
+    content += `\n<details>\n<summary>More information</summary>\n`;
+    content += `### Details\n${vulnerability.details ?? 'No details.'}\n`;
+
+    content += '### Severity\n';
+    const cvssVector =
+      vulnerability.severity?.find((e) => e.type === 'CVSS_V3')?.score ??
+      vulnerability.severity?.[0]?.score;
+    if (cvssVector) {
+      const [baseScore, severity] = this.evaluateCvssVector(cvssVector);
+      const score = baseScore ? `${baseScore} / 10 (${severity})` : 'Unknown';
+      content += `- Score: ${score}\n`;
+      content += `- Vector: \`${cvssVector}\`\n`;
+    } else {
+      content += 'Unknown severity.\n';
+    }
+
+    content += `\n### References\n${
+      vulnerability.references
+        ?.map((ref) => {
+          return `- [${ref.url}](${ref.url})`;
         })
-      );
+        .join('\n') ?? 'No references.'
+    }`;
+
+    let attribution = '';
+    if (vulnerability.id.startsWith('GHSA-')) {
+      attribution = ` and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md))`;
+    } else if (vulnerability.id.startsWith('GO-')) {
+      attribution = ` and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license))`;
+    } else if (vulnerability.id.startsWith('PYSEC-')) {
+      attribution = ` and the [PyPI Advisory Database](https://github.com/pypa/advisory-database) ([CC-BY 4.0](https://github.com/pypa/advisory-database/blob/main/LICENSE))`;
+    } else if (vulnerability.id.startsWith('RUSTSEC-')) {
+      attribution = ` and the [Rust Advisory Database](https://github.com/RustSec/advisory-db) ([CC0 1.0](https://github.com/rustsec/advisory-db/blob/main/LICENSE.txt))`;
+    }
+    content += `\n\nThis data is provided by [OSV](https://osv.dev/vulnerability/${vulnerability.id})${attribution}.\n`;
+    content += `</details>`;
+
+    return [sanitizeMarkdown(content)];
   }
 }
diff --git a/package.json b/package.json
index 1549d63474fad6f0918daaab38bfee445fda539a..74a56adb68f9aacbb7229a3149cc42a83d48bd73 100644
--- a/package.json
+++ b/package.json
@@ -237,6 +237,7 @@
     "upath": "2.0.1",
     "url-join": "4.0.1",
     "validate-npm-package-name": "5.0.0",
+    "vuln-vects": "1.1.0",
     "xmldoc": "1.2.0",
     "zod": "3.20.2"
   },
diff --git a/yarn.lock b/yarn.lock
index b9956f9b511f278dd766c8cad5dee68f7f0d2a87..47d8626137efea12d98c9b7f313ac5417354dcb4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10301,6 +10301,11 @@ vfile@^4.0.0:
     unist-util-stringify-position "^2.0.0"
     vfile-message "^2.0.0"
 
+vuln-vects@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/vuln-vects/-/vuln-vects-1.1.0.tgz#537d403615610446c1d687934584ea9dfb2a63ed"
+  integrity sha512-LGDwn9nRz94YoeqOn2TZqQXzyonBc5FJppSgH34S/1U+3bgPONq/vvfiCbCQ4MeBll58xx+kDmhS73ac+EHBBw==
+
 walk-up-path@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-1.0.0.tgz#d4745e893dd5fd0dbb58dd0a4c6a33d9c9fec53e"