Skip to content

Commit

Permalink
Support cross-platform applications validation (Netflix-Skunkworks#172)
Browse files Browse the repository at this point in the history
* Fix failing Accessible component test

Previous failures were being swallowed by ReactDOM.render.
Consideration should be given to the suggestion to add an Error Boundary
to this component test: https://reactjs.org/docs/error-boundaries.html

* Add "platform" to ApplicationRequirement

Improve type comment hints

* Pull-up os filtering to Security.applications

* Implement linux app kmd source

* Improve tests for security applications resolvers

* Implement LinuxSecurity.applications resolver

* Coerce Debian package versions into semver

This is a bit of a Procrustean bed... Debian package version strings are
all over the place, and long-predate notions of "semantic versioning".

Still, we should make a best-effort to match on the leading version
numbers of available packages, and document this caveat for the user.

* Clarify platform filtering behavior
  • Loading branch information
erichs authored and nathancharles committed Nov 18, 2019
1 parent 60d03b5 commit 473180d
Show file tree
Hide file tree
Showing 21 changed files with 612 additions and 128 deletions.
74 changes: 52 additions & 22 deletions docs/POLICIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,41 +213,71 @@ Application requirements have their own GraphQL schema:

```graphql
# Defines a requirement for an installed application
input AppRequirement {
# required application name
name: String!
# optional application version requirement
version: Semver
# optional platform if only required for specific OS

input ApplicationRequirement {
name: String! # e.g. Slack.app
paths: ApplicationPaths
platform: PlatformStringRequirement
# controls whether regex or equality check is performed against application name
exactMatch: Boolean
# controls whether bin packages are checked (homebrew, chocolatey, etc)
includePackages: Boolean
# install URL
url: String
# explanation to show user
version: Semver
assertion: RequirementOption! # ALWAYS, NEVER, SUGGESTED, etc.
description: String
installFrom: String
}
```

`name` is the only required property. You can specify requirements as an array of `AppRequirements`.
`name` is the only required property. If you do not specify a platform filter, or specify `all: true` in your platform filter, the application requirement will be checked for all platforms. Since application package names, locations, and versions vary across platforms, you are advised to scope your application checks to the specific platforms you care about. You can specify requirements as an array of `ApplicationRequirements`.

**Example Usage:**

```json
{
"requiredApplications": [{
"name": "Google Chrome",
"version": ">=68.0.3440",
"url": "https://www.google.com/chrome/",
"description": "Google Chrome is a secure browser..."
}]
}
"applications": [
{
"name": "CommonApp",
"description": "Should be checked for on all devices",
"assertion": "SUGGESTED"
},
{
"name": "Terminal",
"description": "Terminal.app, present with all MacOS versions",
"assertion": "ALWAYS",
"platform": {
"darwin": ">=10.0.0"
}
},
{
"name": "TV",
"description": "TV.app, introduced with MacOS Catalina",
"assertion": "ALWAYS",
"platform": {
"darwin": ">=10.15.0"
},
"paths": {
"darwin": "/System/Applications"
}
},
{
"name": "bash",
"description": "Bourne Again Shell",
"assertion": "ALWAYS",
"platform": {
"linux": ">=12.04.0"
}
},
{
"name": "Notepad.exe",
"description": "Default Win32 Editor",
"assertion": "ALWAYS",
"platform": {
"win32": ">=10.0.0"
}
}
]
```

### `openWifiConnections`

NOTE: Currently supported on MacOS only

Checks if there are old wifi connections cached locally. This practice uses the `RequirementOption` enum to specify the requirement.

Valid values are: `ALWAYS`, `SUGGESTED`, `NEVER`, `IF_SUPPORTED`
Expand Down
Binary file added j1agent/j1-endpoint-agent-darwin
Binary file not shown.
Binary file added j1agent/j1-endpoint-agent-linux
Binary file not shown.
Binary file added j1agent/j1-endpoint-agent-windows.exe
Binary file not shown.
15 changes: 4 additions & 11 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,10 @@ type SecurityInfo {
# Windows
domainFirewall: FeatureState
}
#
# type Application {
# name: String!
# displayName: String
# version: String!
# lastOpenedTime: String
# installDate: String
# }

type Application {
name: String! # e.g. Slack.app
path: String # ~/Applications
path: String # ~/Applications for Mac, or Win32 Registry path for Windows
version: Semver
assertion: RequirementOption! # ALWAYS, NEVER, SUGGESTED, etc.
description: String
Expand All @@ -105,13 +97,14 @@ type Application {

input ApplicationPaths {
darwin: String # "/Applications"
win32: String # "\Program Files"
win32: String # "HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall"
linux: String # "/usr/share"
}

input ApplicationRequirement {
name: String! # e.g. Slack.app
paths: ApplicationPaths
platform: PlatformStringRequirement
version: Semver
assertion: RequirementOption! # ALWAYS, NEVER, SUGGESTED, etc.
description: String
Expand Down Expand Up @@ -256,7 +249,7 @@ input PlatformStringRequirement {
ubuntu: Semver
linux: Semver
awsWorkspace: Semver
all: Semver
all: Boolean
}

input PlatformBracketRequirement {
Expand Down
3 changes: 2 additions & 1 deletion src/Accessible.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React from 'react'
import ReactDOM from 'react-dom'
import { mount } from 'enzyme'
import Accessible from './Accessible'
import TestRenderer from 'react-test-renderer';

it('renders without crashing', () => {
const div = document.createElement('div')
Expand All @@ -14,7 +15,7 @@ it('renders without crashing', () => {
it('crashes if multiple children are passed', (done) => {
const div = document.createElement('div')
try {
ReactDOM.render(
TestRenderer.create(
<Accessible>
<div />
<div />
Expand Down
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export {
HOST,
DEFAULT_DARWIN_APP_PATH,
DEFAULT_WIN32_APP_PATH,
DEFAULT_WIN32_APP_REGISTRY_PATH,
DEFAULT_LINUX_APP_PATH,
VALID,
INVALID_INSTALL_STATE,
Expand Down
17 changes: 17 additions & 0 deletions src/lib/applicationPlatformFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import kmd from './kmd'
import semver from './patchedSemver'

// Filter applications array (specified in validation policy), return only those
// elements appropriate for the running OS platform/version
export default async function applicationPlatformFilter(applications = [], context, platform, version) {
const osPlatform = platform || process.platform
const osVersion = version || (await kmd('os', context)).system.version

return applications.filter((app) => {
if (!app.platform || app.platform.all) {
return true
}
const platformStringRequirement = app.platform[osPlatform]
return platformStringRequirement && semver.satisfies(osVersion, platformStringRequirement)
})
}
112 changes: 112 additions & 0 deletions src/lib/applicationPlatformFilter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import applicationPlatformFilter from './applicationPlatformFilter'

// validation policy fixture for application checks
const apps = [
{
name: "CommonApp",
description: "App found on all platforms",
},
{
name: "CommonAppWithExplicitAll",
description: "App found on all platforms",
platform: {
all: true
}
},
{
name: "PoorlyFilteredApp",
description: "Doesn't apply to any platforms",
platform: {
all: false
}
},
{
name: "Terminal",
description: "Terminal.app, present with all MacOS versions",
platform: {
darwin: ">=10.0.0"
}
},
{
name: "TV",
description: "TV.app, introduced with MacOS Catalina",
platform: {
darwin: ">=10.15.0"
},
paths: {
darwin: "/System/Applications"
}
},
{
name: "bash",
description: "Bourne Again Shell",
platform: {
linux: ">=12.04.0"
}
},
{
name: "Notepad.exe",
description: "Default Win32 Editor",
platform: {
win32: ">=10.0.0"
}
}
]

describe('applicationPlatformFilter', () => {
it('should return three apps for MacOS Sierra', async () => {
const filteredApps = await applicationPlatformFilter(apps, {}, 'darwin', '10.12.1')
expect(filteredApps.length).toEqual(3)
expect(filteredApps[0].name).toEqual('CommonApp')
expect(filteredApps[1].name).toEqual('CommonAppWithExplicitAll')
expect(filteredApps[2].name).toEqual('Terminal')
})

it('should return four apps for MacOS Catalina', async () => {
const filteredApps = await applicationPlatformFilter(apps, {}, 'darwin', '10.15')
expect(filteredApps.length).toEqual(4)
expect(filteredApps[0].name).toEqual('CommonApp')
expect(filteredApps[1].name).toEqual('CommonAppWithExplicitAll')
expect(filteredApps[2].name).toEqual('Terminal')
expect(filteredApps[3].name).toEqual('TV')
})

it('should return three apps for Ubuntu Xenial ', async () => {
const filteredApps = await applicationPlatformFilter(apps, {}, 'linux', '16.04')
expect(filteredApps.length).toEqual(3)
expect(filteredApps[0].name).toEqual('CommonApp')
expect(filteredApps[1].name).toEqual('CommonAppWithExplicitAll')
expect(filteredApps[2].name).toEqual('bash')
})

it('should return two apps for Ubuntu Hardy ', async () => {
const filteredApps = await applicationPlatformFilter(apps, {}, 'linux', '8.04')
expect(filteredApps.length).toEqual(2)
expect(filteredApps[0].name).toEqual('CommonApp')
expect(filteredApps[1].name).toEqual('CommonAppWithExplicitAll')
})

it('should return three apps for Windows 10', async () => {
const filteredApps = await applicationPlatformFilter(apps, {}, 'win32', '10.0')
expect(filteredApps.length).toEqual(3)
expect(filteredApps[0].name).toEqual('CommonApp')
expect(filteredApps[1].name).toEqual('CommonAppWithExplicitAll')
expect(filteredApps[2].name).toEqual('Notepad.exe')
})

it('should return two apps for Windows 7', async () => {
const filteredApps = await applicationPlatformFilter(apps, {}, 'win32', '6.1')
expect(filteredApps.length).toEqual(2)
expect(filteredApps[0].name).toEqual('CommonApp')
expect(filteredApps[1].name).toEqual('CommonAppWithExplicitAll')
})

it('should not return PoorlyFilteredApp', async () => {
const macApps = await applicationPlatformFilter(apps, {}, 'darwin', '10.12')
const linApps = await applicationPlatformFilter(apps, {}, 'linux', '8.04')
const winApps = await applicationPlatformFilter(apps, {}, 'win32', '6.1')
expect(macApps.some(app => app.name == 'PoorlyFilteredApp')).toEqual(false)
expect(linApps.some(app => app.name == 'PoorlyFilteredApp')).toEqual(false)
expect(winApps.some(app => app.name == 'PoorlyFilteredApp')).toEqual(false)
})
})
6 changes: 6 additions & 0 deletions src/lib/sanitizeDebianVersionString.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default function sanitizeDebianVersionString(version) {
return version
.replace(/^\d+:/, '') // trim leading epoch numbers
.replace(/[^\d.].*$/, '') // trim trailing debian-revision strings
.replace(/(\d+\.\d+\.\d+).*/, '$1') // trim remaining upstream-version to a semver
}
23 changes: 23 additions & 0 deletions src/lib/sanitizeDebianVersionString.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import sanitizeDebianVersionString from './sanitizeDebianVersionString'

describe('sanitizeDebianVersionString', () => {

it('should not change semver-compatible versions', () => {
const sanitized = sanitizeDebianVersionString('1.2.3')
expect(sanitized).toEqual('1.2.3')
})

it('should remove leading epochs', () => {
const sanitized = sanitizeDebianVersionString('2:1.0.0')
expect(sanitized).toEqual('1.0.0')
})

it('should remove trailing debian revisions', () => {
expect(sanitizeDebianVersionString('1:13.3.0-2build1~18.04.1')).toEqual('13.3.0')
expect(sanitizeDebianVersionString('2:8.39-9')).toEqual('8.39')
expect(sanitizeDebianVersionString('2:8.39-9')).toEqual('8.39')
expect(sanitizeDebianVersionString('3.28.0.2-1ubuntu1.18.04.1')).toEqual('3.28.0') // NOTE: lossy match
expect(sanitizeDebianVersionString('1.1.24+nmu5ubuntu1')).toEqual('1.1.24')
expect(sanitizeDebianVersionString('1:1.2.11.dfsg-0ubuntu2')).toEqual('1.2.11')
})
})
8 changes: 6 additions & 2 deletions src/resolvers/Security.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
import kmd from '../lib/kmd'
import { PlatformSecurity } from './platform/'
import config from '../config'
import applicationPlatformFilter from '../lib/applicationPlatformFilter'


export default {
async automaticAppUpdates (root, args, context) {
Expand Down Expand Up @@ -163,9 +165,11 @@ export default {
return UNSUPPORTED
},

async applications (root, args, context) {
async applications (root, args, context, osPlatform, osVersion) {
if ('applications' in PlatformSecurity) {
const results = await PlatformSecurity.applications(root, args, context)
const platformApps = await applicationPlatformFilter(args.applications, context, osPlatform, osVersion)

const results = await PlatformSecurity.applications(root, platformApps, context)

return results.map((data, idx) => {
const config = args.applications[idx]
Expand Down
Loading

0 comments on commit 473180d

Please sign in to comment.