Repository: lirantal/lockfile-lint
Pull request #52 (2020-01-26 23:13:58)

feat(validators): add URL validator
address #39

## Description

Allows to specify more specific URLs in `--allowed-hosts` option by specifying something like `github.com/SomeOrg/SomeRepo#<hash>`. You can be as specific as you want, e.g. `github.com`, `github.com/SomeOrg`, `github.com/SomeOrg/SomeRepo` and `github.com/SomeOrg/SomeRepo#<hash>` are all valid options.

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Related Issue

#11 

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [ ] I have updated the documentation (if required).
- [x] I have read the **CONTRIBUTING** document.
- [ ] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [ ] I added a picture of a cute animal cause it's fun


codecov-io: # [Codecov](https://codecov.io/gh/lirantal/lockfile-lint/pull/52?src=pr&el=h1) Report
> Merging [#52](https://codecov.io/gh/lirantal/lockfile-lint/pull/52?src=pr&el=desc) into [master](https://codecov.io/gh/lirantal/lockfile-lint/commit/6f0bb4e96f2cf9635dc56c654cbc3ad80c0de4bd?src=pr&el=desc) will **increase** coverage by `0.02%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/lirantal/lockfile-lint/pull/52/graphs/tree.svg?width=650&token=P36VJa7ksa&height=150&src=pr)](https://codecov.io/gh/lirantal/lockfile-lint/pull/52?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master     #52      +/-   ##
=========================================
+ Coverage   97.88%   97.9%   +0.02%     
=========================================
  Files          11      11              
  Lines         189     191       +2     
  Branches       29      29              
=========================================
+ Hits          185     187       +2     
  Misses          4       4
```


| [Impacted Files](https://codecov.io/gh/lirantal/lockfile-lint/pull/52?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [...s/lockfile-lint-api/src/validators/ValidateHost.js](https://codecov.io/gh/lirantal/lockfile-lint/pull/52/diff?src=pr&el=tree#diff-cGFja2FnZXMvbG9ja2ZpbGUtbGludC1hcGkvc3JjL3ZhbGlkYXRvcnMvVmFsaWRhdGVIb3N0Lmpz) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/lirantal/lockfile-lint/pull/52?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/lirantal/lockfile-lint/pull/52?src=pr&el=footer). Last update [6f0bb4e...a0a6355](https://codecov.io/gh/lirantal/lockfile-lint/pull/52?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

lirantal: @bolatovumar thanks for sending over. Let's discuss further in #11 
lirantal: @bolatovumar are you up for some changes in order to land this in?
lirantal: @bolatovumar putting my thoughts around enabling this capability without coupling it into the existing hosts validation logic, see below:

* We'll support a new `--allowed-urls` that will specify full URLs that would have to match.
For example, this should work:

```
  --allowed-hosts npm.com
  --allowed-urls https://github.com/user/repo/?branch=master&commit=1234
```

Acceptance tests:
1. Also just specifying the `--allowed-urls` should work regardless of `--allowed-hosts`.
2. Matching the URL should be best-effort exact match, which is the same as you implemented it in the PR currently - https://github.com/lirantal/lockfile-lint/pull/52/files#diff-803bbd31414b876c6a30b1dcf035cac8R51

* First change is in `lockfile-lint-api` package, by introducing a new validator for URLs. Copying from `src/validators/ValidateHost.js` should be a good start.

* The second change is in `lockfile-lint` package to add the command line arg (`src/cli.js`), update the existing hosts validator (the one in `src/validators/index.js`) to actually also check the url option as well, and manage the errors thrown to also really throw if one of --allowed-hosts or --allowed-urls throws
bolatovumar: @lirantal makes sense. I will probably have some time to look at this in a week or so. Was a bit busy in recent weeks.
lirantal: Ok, let's see how it goes in the upcoming week.
If you see you're not getting to it anytime soon let me know and I'll pick it up
bolatovumar: Ok, I added a separate URL validator which tries to do an exact match for a URL in the list of allowed URLs. However, in its current state it ONLY checks for a list of passed-in URLs and is not usable together with `--allowed-hosts` flag. I need to get them to work together next.
lirantal: @bolatovumar see my last bullet. The new validator you add to the api package is ok to be standalone and doesn't need to "play nicely together with allowed-hosts" because that's an API and the consumer will implement whatever they want. The change of making them play nicely together is on the CLI package and specifically inside `src/validators/index.js`. Read that last bullet I mentioned and see if it helps to clear up any vagueness as how to implement it.
bolatovumar: @lirantal still not entirely sure what you mean. As it works currently, `ValidateHost` and `ValidateUrl` classes are completely independent. `ValidateHost` `validate` method receives an array of valid hosts and `ValidateUrl` `validate` method receives an array of allowed URLs. Now, if you specify something like:

> --allowed-hosts npm.com
> --allowed-urls https://github.com/user/repo/?branch=master&commit=1234 

the `ValidateHost` `validate` method will add errors for all packages which have the url of `https://github.com/user/repo/?branch=master&commit=1234` since it's not aware of the parameters passed to `--allowed-urls`. Same thing for `--allowed-urls`. If will generate errors for all packages which resolve to something other than `https://github.com/user/repo/?branch=master&commit=1234`.
lirantal: @bolatovumar ok, so once more - you should add to the `lockfile-lint-api` package the individual class that validates a URL. That is indeed a separate class.
To the `lockfile-lint` package, there's a `src/validators/index.js` file which has a `ValidateHostManager` function. That is the one which you need to test for both a host and a URL and then do the logic inside to make sure that even if the host passes, the URL may fail it.

Does that help make it clear? :)
bolatovumar: @lirantal `ValidateHostManager` receives values passed to the `--allowed-hosts` flag like `npm`. Should it also receive values passed to the `--allowed-urls` flag then? 

If you have something like `--allowed-hosts npm --allowed-urls https://github.com/some-repo` then if what I'm describing above is correct `ValidateHostManager` will receive `['npm', 'https://github.com/some-repo']` as validator values instead of just  `['npm']` as it does now. 

Then `['npm', 'https://github.com/some-repo']` values would be passed along to `ValidateHostManager`'s `validate` method which would check if all of the packages pass at least one of the validator requirements (i.e., either they are on npm or on https://github.com/some-repo).

Same as above would have to be done for `ValidateUrlManager` (it accepts values for both allowed hosts and urls, then checks that all packages pass at least one of the validator requirements).
lirantal: > @lirantal ValidateHostManager receives values passed to the --allowed-hosts flag like npm. Should it also receive values passed to the --allowed-urls flag then?

Exactly. Change it to also receive that flag, and then when it has both it can call the underlying API for both of them and make the proper decision.

I think you got it ;-)
lirantal: @bolatovumar thanks for keeping at it. We're getting there and this is really close, just need to polish out the way this validator now works for the CLI
lirantal: @bolatovumar sorry about the delay here. Great work and commitment from you. I wanted to say that I appreciate the time and attending to everything! 🙏

Patch:
diff --git a/packages/lockfile-lint-api/index.js b/packages/lockfile-lint-api/index.js
index 12adaef..300d4e5 100644
--- a/packages/lockfile-lint-api/index.js
+++ b/packages/lockfile-lint-api/index.js
@@ -3,11 +3,13 @@
 const ValidateHost = require('./src/validators/ValidateHost')
 const ValidateHttps = require('./src/validators/ValidateHttps')
 const ValidateScheme = require('./src/validators/ValidateScheme')
+const ValidateUrl = require('./src/validators/ValidateUrl')
 const ParseLockfile = require('./src/ParseLockfile')
 
 module.exports = {
   ParseLockfile,
   ValidateHost,
   ValidateHttps,
-  ValidateScheme
+  ValidateScheme,
+  ValidateUrl
 }
diff --git a/packages/lockfile-lint-api/src/validators/ValidateHost.js b/packages/lockfile-lint-api/src/validators/ValidateHost.js
index 24390f1..d033396 100644
--- a/packages/lockfile-lint-api/src/validators/ValidateHost.js
+++ b/packages/lockfile-lint-api/src/validators/ValidateHost.js
@@ -28,17 +28,14 @@ module.exports = class ValidateHost {
         continue
       }
 
-      let packageResolvedURL = {}
-
       try {
-        packageResolvedURL = new URL(packageMetadata.resolved)
-
+        const packageResolvedURL = new URL(packageMetadata.resolved)
         const allowedHosts = hosts.map(hostValue => {
           // eslint-disable-next-line security/detect-object-injection
           return REGISTRY[hostValue] ? REGISTRY[hostValue] : hostValue
         })
-
-        if (!allowedHosts.includes(packageResolvedURL.host)) {
+        const isPassing = allowedHosts.includes(packageResolvedURL.host)
+        if (!isPassing) {
           if (!packageResolvedURL.host && options && options.emptyHostname) {
             debug(`detected empty hostname but allowing because emptyHostname is not false`)
           } else {
@@ -61,4 +58,21 @@ module.exports = class ValidateHost {
 
     return validationResult
   }
+
+  validateSingle (packageName, hosts) {
+    // eslint-disable-next-line security/detect-object-injection
+    const packageMetadata = this.packages[packageName]
+
+    if (!('resolved' in packageMetadata)) {
+      return true
+    }
+
+    const packageResolvedURL = new URL(packageMetadata.resolved)
+    const allowedHosts = hosts.map(hostValue => {
+      // eslint-disable-next-line security/detect-object-injection
+      return REGISTRY[hostValue] ? REGISTRY[hostValue] : hostValue
+    })
+
+    return allowedHosts.includes(packageResolvedURL.host)
+  }
 }
diff --git a/packages/lockfile-lint-api/src/validators/ValidateUrl.js b/packages/lockfile-lint-api/src/validators/ValidateUrl.js
new file mode 100644
index 0000000..314236e
--- /dev/null
+++ b/packages/lockfile-lint-api/src/validators/ValidateUrl.js
@@ -0,0 +1,60 @@
+'use strict'
+
+module.exports = class ValidateUrl {
+  constructor ({packages} = {}) {
+    if (typeof packages !== 'object') {
+      throw new Error('expecting an object passed to validator constructor')
+    }
+
+    this.packages = packages
+  }
+
+  validate (allowedUrls, options) {
+    if (!Array.isArray(allowedUrls)) {
+      throw new Error('validate method requires an array')
+    }
+
+    let validationResult = {
+      type: 'success',
+      errors: []
+    }
+
+    for (const [packageName, packageMetadata] of Object.entries(this.packages)) {
+      if (!('resolved' in packageMetadata)) {
+        continue
+      }
+
+      try {
+        const isPassing = allowedUrls.indexOf(packageMetadata.resolved) > -1
+        if (!isPassing) {
+          validationResult.errors.push({
+            message: `detected invalid url(s) for package: ${packageName}\n    expected: ${allowedUrls}\n    actual: ${
+              packageMetadata.resolved
+            }\n`,
+            package: packageName
+          })
+        }
+      } catch (error) {
+        // swallow error (assume that the version is correct)
+      }
+    }
+
+    if (validationResult.errors.length !== 0) {
+      validationResult.type = 'error'
+    }
+
+    return validationResult
+  }
+
+  validateSingle (packageName, allowedUrls) {
+    // eslint-disable-next-line security/detect-object-injection
+    const packageMetadata = this.packages[packageName]
+    if (!('resolved' in packageMetadata)) {
+      return true
+    }
+
+    const resolvedUrl = packageMetadata.resolved
+
+    return allowedUrls.indexOf(resolvedUrl) > -1
+  }
+}
diff --git a/packages/lockfile-lint/bin/lockfile-lint.js b/packages/lockfile-lint/bin/lockfile-lint.js
index 167363b..bf4d993 100755
--- a/packages/lockfile-lint/bin/lockfile-lint.js
+++ b/packages/lockfile-lint/bin/lockfile-lint.js
@@ -18,17 +18,32 @@ let validators = []
 const supportedValidators = new Map([
   ['allowed-hosts', 'validateHosts'],
   ['validate-https', 'validateHttps'],
-  ['allowed-schemes', 'validateSchemes']
+  ['allowed-schemes', 'validateSchemes'],
+  ['allowed-urls', 'validateUrls']
 ])
 
 for (const [commandArgument, commandValue] of Object.entries(config)) {
+  /**
+   * If we have both --allowed-urls and --allowed-hosts flags active
+   * then we can skip doing the work for allowed urls as the validator
+   * for allowed hosts will check for both.
+   *
+   * We only need to run the check for allowed urls if the user does not
+   * specify allowed hosts.
+   */
+  if (commandArgument === 'allowed-urls' && config['allowed-hosts']) {
+    continue
+  }
+
   if (supportedValidators.has(commandArgument)) {
     const validatorItem = supportedValidators.get(commandArgument)
     validators.push({
       name: validatorItem,
       values: commandValue,
       options: {
-        emptyHostname: config['empty-hostname']
+        emptyHostname: config['empty-hostname'],
+        allowedHosts: config['allowed-hosts'],
+        allowedUrls: config['allowed-urls']
       }
     })
   }
diff --git a/packages/lockfile-lint/src/config.js b/packages/lockfile-lint/src/config.js
index ff29ddd..fe619d4 100644
--- a/packages/lockfile-lint/src/config.js
+++ b/packages/lockfile-lint/src/config.js
@@ -62,6 +62,11 @@ module.exports = (argv, exitProcess = false, searchFrom = process.cwd()) => {
         describe:
           'validates a whitelist of allowed schemes to be used for resources in the lockfile',
         conflicts: ['validate-https', 's']
+      },
+      u: {
+        alias: ['allowed-urls'],
+        type: 'array',
+        describe: 'validates a whitelist of allowed URLs to be used for resources in the lockfile'
       }
     })
     .example('lockfile-lint --path yarn.lock --validate-https')
diff --git a/packages/lockfile-lint/src/main.js b/packages/lockfile-lint/src/main.js
index d0a99a0..cb09276 100644
--- a/packages/lockfile-lint/src/main.js
+++ b/packages/lockfile-lint/src/main.js
@@ -4,13 +4,15 @@ const debug = require('debug')('lockfile-lint')
 const {
   ValidateHostManager,
   ValidateHttpsManager,
-  ValidateSchemeManager
+  ValidateSchemeManager,
+  ValidateUrlManager
 } = require('../src/validators')
 
 const validatorFunctions = new Map([
   ['validateHosts', ValidateHostManager],
   ['validateHttps', ValidateHttpsManager],
-  ['validateSchemes', ValidateSchemeManager]
+  ['validateSchemes', ValidateSchemeManager],
+  ['validateUrls', ValidateUrlManager]
 ])
 
 function runValidators ({type, path, validators} = {}) {
diff --git a/packages/lockfile-lint/src/validators/index.js b/packages/lockfile-lint/src/validators/index.js
index f81e485..5b8ad5e 100644
--- a/packages/lockfile-lint/src/validators/index.js
+++ b/packages/lockfile-lint/src/validators/index.js
@@ -1,12 +1,19 @@
 'use strict'
 
-const {ValidateHost, ParseLockfile, ValidateHttps, ValidateScheme} = require('lockfile-lint-api')
+const {
+  ValidateHost,
+  ParseLockfile,
+  ValidateHttps,
+  ValidateScheme,
+  ValidateUrl
+} = require('lockfile-lint-api')
 const debug = require('debug')
 
 module.exports = {
   ValidateHostManager,
   ValidateHttpsManager,
-  ValidateSchemeManager
+  ValidateSchemeManager,
+  ValidateUrlManager
 }
 
 function ValidateSchemeManager ({path, type, validatorValues, validatorOptions}) {
@@ -39,8 +46,23 @@ function ValidateHostManager ({path, type, validatorValues, validatorOptions}) {
   const parser = new ParseLockfile(options)
   const lockfile = parser.parseSync()
   const validator = new ValidateHost({packages: lockfile.object})
+  const validationResult = validator.validate(validatorValues, validatorOptions)
 
-  return validator.validate(validatorValues, validatorOptions)
+  // Check if some of the errors are for allowed URLs and filter those out
+  if (validatorOptions && validatorOptions.allowedUrls) {
+    const urlValidator = new ValidateUrl({packages: lockfile.object})
+
+    validationResult.errors = validationResult.errors.filter(
+      result => !urlValidator.validateSingle(result.package, validatorOptions.allowedUrls)
+    )
+
+    // If we don't have any errors left at this point make sure it's a success type
+    if (!validationResult.errors.length) {
+      validationResult.type = 'success'
+    }
+  }
+
+  return validationResult
 }
 
 function ValidateHttpsManager ({path, type, validatorValues, validatorOptions}) {
@@ -59,3 +81,20 @@ function ValidateHttpsManager ({path, type, validatorValues, validatorOptions})
 
   return validator.validate()
 }
+
+function ValidateUrlManager ({path, type, validatorValues, validatorOptions}) {
+  debug('validate-url-manager')(
+    `invoked with validator options: ${JSON.stringify(validatorValues)}`
+  )
+
+  const options = {
+    lockfilePath: path,
+    lockfileType: type
+  }
+
+  const parser = new ParseLockfile(options)
+  const lockfile = parser.parseSync()
+  const validator = new ValidateUrl({packages: lockfile.object})
+
+  return validator.validate(validatorValues, validatorOptions)
+}


Test patch:
diff --git a/packages/lockfile-lint-api/__tests__/validators.host.test.js b/packages/lockfile-lint-api/__tests__/validators.host.test.js
index 685b6da..8b16d40 100644
--- a/packages/lockfile-lint-api/__tests__/validators.host.test.js
+++ b/packages/lockfile-lint-api/__tests__/validators.host.test.js
@@ -197,4 +197,24 @@ describe('Validator: Host', () => {
       validator.validate(['npm'])
     }).not.toThrow()
   })
+
+  it('validator should return true for a single package on a valid host', () => {
+    const mockedPackages = {
+      '@babel/code-frame': {
+        resolved: 'https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz'
+      }
+    }
+    const validator = new ValidatorHost({packages: mockedPackages})
+
+    expect(validator.validateSingle('@babel/code-frame', ['npm'])).toEqual(true)
+  })
+
+  it('validator should return true for a single package that does not have a resolved URL', () => {
+    const mockedPackages = {
+      meow: {}
+    }
+    const validator = new ValidatorHost({packages: mockedPackages})
+
+    expect(validator.validateSingle('meow', ['npm'])).toEqual(true)
+  })
 })
diff --git a/packages/lockfile-lint-api/__tests__/validators.url.test.js b/packages/lockfile-lint-api/__tests__/validators.url.test.js
new file mode 100644
index 0000000..b6c4d4c
--- /dev/null
+++ b/packages/lockfile-lint-api/__tests__/validators.url.test.js
@@ -0,0 +1,138 @@
+const ValidatorUrl = require('../src/validators/ValidateUrl')
+
+describe('Validator: Url', () => {
+  it('validator should throw an error when provided a string', () => {
+    expect(() => new ValidatorUrl('ss')).toThrowError()
+  })
+
+  it('validator should throw an error when provided null', () => {
+    expect(() => new ValidatorUrl(null)).toThrowError()
+  })
+
+  it('validator should throw an error when provided array', () => {
+    expect(() => new ValidatorUrl(['a'])).toThrowError()
+  })
+
+  it('validator should throw an error instantiated with no value', () => {
+    expect(() => new ValidatorUrl()).toThrowError()
+  })
+
+  it('validator should fail if not allowed url is used for a resource', () => {
+    const mockedPackages = {
+      bolt11: {
+        resolved: 'https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247'
+      }
+    }
+
+    const validator = new ValidatorUrl({packages: mockedPackages})
+    expect(validator.validate(['https://registry.npmjs.org'])).toEqual({
+      type: 'error',
+      errors: [
+        {
+          message: `detected invalid url(s) for package: bolt11\n    expected: https://registry.npmjs.org\n    actual: https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247\n`,
+          package: 'bolt11'
+        }
+      ]
+    })
+  })
+
+  it('validator should succeed if all resources are from an allowed URL', () => {
+    const mockedPackages = {
+      '@babel/code-frame': {
+        resolved: 'https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz'
+      },
+      meow: {
+        resolved: 'https://registry.npmjs.org/meow/-/meow-4.0.1.tgz'
+      },
+      '@babel/generator': {
+        resolved: 'https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz'
+      }
+    }
+
+    const validator = new ValidatorUrl({packages: mockedPackages})
+    expect(
+      validator.validate([
+        'https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz',
+        'https://registry.npmjs.org/meow/-/meow-4.0.1.tgz',
+        'https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz'
+      ])
+    ).toEqual({
+      type: 'success',
+      errors: []
+    })
+  })
+
+  it('validator should fail if validate method receives a non-array value', () => {
+    const mockedPackages = {
+      '@babel/code-frame': {
+        resolved: 'https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz'
+      },
+      meow: {
+        resolved: 'http://registry.npmjs.org/meow/-/meow-4.0.1.tgz'
+      },
+      '@babel/generator': {
+        resolved: 'https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz'
+      }
+    }
+
+    const validator = new ValidatorUrl({packages: mockedPackages})
+    expect(() => {
+      validator.validate(null)
+    }).toThrowError(`validate method requires an array`)
+  })
+
+  it('validator should not throw if package has no `resolved` field', () => {
+    const mockedPackages = {
+      '@babel/code-frame': {
+        resolved: 'https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz'
+      },
+      meow: {}
+    }
+    const validator = new ValidatorUrl({packages: mockedPackages})
+
+    expect(() => {
+      validator.validate(['https://registry.npms.org/@babel/code-frame'])
+    }).not.toThrow()
+  })
+
+  it('validator should return true for a single package with a valid URL', () => {
+    const mockedPackages = {
+      'bolt11@https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247': {
+        resolved: 'https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247'
+      }
+    }
+    const validator = new ValidatorUrl({packages: mockedPackages})
+
+    expect(
+      validator.validateSingle(
+        'bolt11@https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247',
+        ['https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247']
+      )
+    ).toEqual(true)
+  })
+
+  it('validator should return false for a single package with an invalid URL', () => {
+    const mockedPackages = {
+      'bolt11@https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247': {
+        resolved: 'https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247'
+      }
+    }
+    const validator = new ValidatorUrl({packages: mockedPackages})
+
+    expect(
+      validator.validateSingle(
+        'bolt11@https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247',
+        ['https://github.com/LN-Zap/bolt11']
+      )
+    ).toEqual(false)
+  })
+
+  it('validator should return true for a single package that does not have a resolved URL', () => {
+    const mockedPackages = {
+      meow: {}
+    }
+    const validator = new ValidatorUrl({packages: mockedPackages})
+
+    expect(validator.validateSingle('meow', ['https://github.com/LN-Zap/bolt11'])).toEqual(true)
+  })
+})
diff --git a/packages/lockfile-lint/__tests__/cli.test.js b/packages/lockfile-lint/__tests__/cli.test.js
index 4796009..930ec03 100644
--- a/packages/lockfile-lint/__tests__/cli.test.js
+++ b/packages/lockfile-lint/__tests__/cli.test.js
@@ -4,6 +4,7 @@ const path = require('path')
 // eslint-disable-next-line security/detect-child-process
 const childProcess = require('child_process')
 const cliExecPath = path.join(__dirname, '../bin/lockfile-lint.js')
+const {ValidateHostManager, ValidateUrlManager} = require('../src/validators/index')
 
 describe('CLI tests', () => {
   test('Running without parameters should display help', done => {
@@ -101,6 +102,29 @@ describe('CLI tests', () => {
     })
   })
 
+  test('Allowed hosts and allowed urls flags should work together', done => {
+    const process = childProcess.spawn(cliExecPath, [
+      '--type',
+      'yarn',
+      '--path',
+      '__tests__/fixtures/yarn-and-github-url.lock',
+      '--allowed-hosts',
+      'yarn',
+      '--allowed-urls',
+      'https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247'
+    ])
+
+    let output = ''
+    process.stderr.on('data', chunk => {
+      output += chunk
+    })
+
+    process.stderr.on('close', _ => {
+      expect(output).toBe('')
+      done()
+    })
+  })
+
   describe('cosmiconfig integration', () => {
     it('options are loaded from cosmiconfig files', done => {
       const lintProcess = childProcess.spawn(cliExecPath, [], {
@@ -155,3 +179,58 @@ describe('CLI tests', () => {
     })
   })
 })
+
+describe('Validator managers:', () => {
+  it('Host manager should work together with URL manager', () => {
+    const result = ValidateHostManager({
+      path: '__tests__/fixtures/yarn-and-github-url.lock',
+      type: 'yarn',
+      validatorValues: ['yarn'],
+      validatorOptions: {
+        allowedUrls: ['https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247']
+      }
+    })
+    expect(result).toEqual({
+      type: 'success',
+      errors: []
+    })
+  })
+
+  it('Host manager should return errors for lock file with packages on other hosts', () => {
+    const result = ValidateHostManager({
+      path: '__tests__/fixtures/yarn-and-github-url.lock',
+      type: 'yarn',
+      validatorValues: ['yarn']
+    })
+    expect(result).toEqual({
+      type: 'error',
+      errors: [
+        {
+          message:
+            'detected invalid host(s) for package: bolt11@https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247\n    expected: registry.yarnpkg.com\n    actual: github.com\n',
+          package:
+            'bolt11@https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247'
+        }
+      ]
+    })
+  })
+
+  it('URL manager should return errors for lock file with packages on other URLs', () => {
+    const result = ValidateUrlManager({
+      path: '__tests__/fixtures/yarn-and-github-url.lock',
+      type: 'yarn',
+      validatorValues: ['https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247']
+    })
+
+    expect(result).toEqual({
+      type: 'error',
+      errors: [
+        {
+          message:
+            'detected invalid url(s) for package: ms@^2.1.1\n    expected: https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247\n    actual: https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009\n',
+          package: 'ms@^2.1.1'
+        }
+      ]
+    })
+  })
+})
diff --git a/packages/lockfile-lint/__tests__/fixtures/yarn-and-github-url.lock b/packages/lockfile-lint/__tests__/fixtures/yarn-and-github-url.lock
new file mode 100644
index 0000000..2b2372d
--- /dev/null
+++ b/packages/lockfile-lint/__tests__/fixtures/yarn-and-github-url.lock
@@ -0,0 +1,21 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+ms@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+"bolt11@https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247":
+  version "1.2.6"
+  resolved "https://github.com/LN-Zap/bolt11#0492874ea9ced4ab49bf0f59a62368687f147247"
+  dependencies:
+    "@types/bn.js" "^4.11.3"
+    bech32 "^1.1.2"
+    bitcoinjs-lib "^3.3.1"
+    bn.js "^4.11.8"
+    coininfo "https://github.com/LN-Zap/coininfo#d7ac7be9c9c4ee74dada187f7762a55887a9c6ca"
+    lodash "^4.17.11"
+    safe-buffer "^5.1.1"
+    secp256k1 "^3.4.0"