Skip to content

Commit

Permalink
Posture Checks for JVM/Android (#97)
Browse files Browse the repository at this point in the history
* add controller API dealing with posture checks

* Implement PostureCheckService

* implement Android Posture service

* submit posture data

* handle security posture failures
  • Loading branch information
ekoby authored Nov 18, 2020
1 parent 50ea998 commit 1e5cc27
Show file tree
Hide file tree
Showing 12 changed files with 426 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2018-2020 NetFoundry, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.openziti.android

import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.openziti.api.PostureQuery
import org.openziti.api.PostureQueryType
import org.openziti.api.PostureResponse
import org.openziti.posture.PostureService

@RunWith(AndroidJUnit4::class)
class PostureProviderTest {

lateinit var postureService: PostureService
@Before
fun setup() {
postureService = PostureService()
}
@Test
fun osPostureTest() {
postureService.registerServiceCheck("servid", PostureQuery(PostureQueryType.OS, "checkid", false, null))

val pr = postureService.getPosture()[0]
assertEquals(PostureQueryType.OS, pr.typeId)
val os = pr.data as PostureResponse.OS
assertEquals("android", os.type)
val verComp = os.version.split(".")
assertEquals(3, verComp.size)
}

@Test
fun macPostureTest() {
postureService.registerServiceCheck("servid", PostureQuery(PostureQueryType.MAC, "checkid", false, null))

val pr = postureService.getPosture()[0]
assertEquals(PostureQueryType.MAC, pr.typeId)
val mac = pr.data as PostureResponse.MAC
assert(mac.macAddresses.isNotEmpty())
}

}
62 changes: 62 additions & 0 deletions ziti-android/src/main/java/org/openziti/android/PostureProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2018-2020 NetFoundry, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.openziti.android

import android.os.Build
import org.openziti.api.PostureQuery
import org.openziti.api.PostureQueryType
import org.openziti.api.PostureResponse
import org.openziti.posture.PostureService
import org.openziti.posture.PostureServiceProvider
import java.net.NetworkInterface

class PostureProvider: PostureServiceProvider {
override fun getPostureService(): PostureService = AndroidPostureService

object AndroidPostureService: PostureService {
val androidVersion: String by lazy {
Build.VERSION.RELEASE.split(".").plus(arrayOf("0","0")).take(3).joinToString(".")
}
val androidBuild: String by lazy {
Build.VERSION.SECURITY_PATCH.replace("-", "") // turn date into an int
}

val macAddresses: Array<String> by lazy {
NetworkInterface.getNetworkInterfaces().toList()
.map { it.hardwareAddress }
.filterNotNull()
.map { it.joinToString(":"){ b -> "%02x".format(b) } }
.toTypedArray()
}

val queries = mutableMapOf<String, PostureQuery>()

override fun registerServiceCheck(serviceId: String, query: PostureQuery) {
queries.put(query.id, query)
}

override fun getPosture(): Array<PostureResponse> = queries.values
.map{ processQuery(it) }
.filterNotNull().toTypedArray()

internal fun processQuery(q: PostureQuery): PostureResponse? = when(q.queryType) {
PostureQueryType.OS -> PostureResponse.OS(q.id, "android", androidVersion, androidBuild)
PostureQueryType.MAC -> PostureResponse.MAC(q.id, macAddresses)
else -> null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#
# Copyright (c) 2018-2020 NetFoundry, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

#
# Copyright (c) 2018-2020 NetFoundry, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#


org.openziti.android.PostureProvider
4 changes: 3 additions & 1 deletion ziti/src/main/kotlin/org/openziti/Exceptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ sealed class Errors {
object NotAuthorized: Errors()
object EdgeRouterUnavailable: Errors()
object ServiceNotAvailable: Errors()
object InsufficientSecurity: Errors()
data class WTF(val err: String): Errors()

override fun toString(): String = javaClass.simpleName
Expand All @@ -33,7 +34,8 @@ private val errorMap = mapOf(
"INVALID_AUTHENTICATION" to Errors.NotAuthorized,
"REQUIRES_CERT_AUTH" to Errors.NotAuthorized,
"UNAUTHORIZED" to Errors.NotAuthorized,
"INVALID_AUTH" to Errors.NotAuthorized
"INVALID_AUTH" to Errors.NotAuthorized,
"INVALID_POSTURE" to Errors.InsufficientSecurity
)

fun getZitiError(err: String): Errors = errorMap.getOrElse(err) { Errors.WTF(err) }
Expand Down
14 changes: 11 additions & 3 deletions ziti/src/main/kotlin/org/openziti/api/Controller.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.openziti.api

import com.google.gson.JsonObject
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -79,6 +80,9 @@ class Controller(endpoint: URL, sslContext: SSLContext, trustManager: X509TrustM
@POST("sessions")
fun createNetworkSession(@Body req: SessionReq): Deferred<Response<Session>>

@POST("posture-response")
fun sendPosture (@Body pr: PostureResponse): Deferred<Response<JsonObject>>

@DELETE("{p}")
fun delete(@Header("zt-session") session: String, @Path("p", encoded = true) path: String): Call<Response<Unit>>
}
Expand Down Expand Up @@ -132,7 +136,7 @@ class Controller(endpoint: URL, sslContext: SSLContext, trustManager: X509TrustM
val resp = call.await()
apiSession = resp.data!!
} catch (ex: Exception) {
return convertError(ex)
convertError(ex)
}
}
return apiSession!!
Expand Down Expand Up @@ -205,11 +209,15 @@ class Controller(endpoint: URL, sslContext: SSLContext, trustManager: X509TrustM
val response = api.createNetworkSession(SessionReq(s.id, t)).await()
return response.data!!
} catch (ex: Exception) {
return convertError(ex)
convertError(ex)
}
}

private fun <T> convertError(t: Throwable): T {
internal suspend fun sendPostureResp(pr: PostureResponse) {
api.sendPosture(pr).await()
}

private fun convertError(t: Throwable): Nothing {
e("error $t", t)
when (t) {
is HttpException -> throw ZitiException(getZitiError(getError(t.response())))
Expand Down
86 changes: 78 additions & 8 deletions ziti/src/main/kotlin/org/openziti/api/types.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,26 @@ package org.openziti.api

import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.annotations.SerializedName
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import java.util.*

internal const val InterceptConfig = "ziti-tunneler-client.v1"
internal enum class SessionType {
Dial,
Bind
}

enum class PostureQueryType {
OS,
MAC,
DOMAIN,
PROCESS
}

internal data class ClientInfo(val sdkInfo: Map<*, *>, val envInfo: Map<*, *>, val configTypes: Array<String>)

internal class Response<T>(val meta: Meta, val data: T?, val error: Error?)
Expand All @@ -28,28 +46,59 @@ internal class Meta(val pagination: Pagination?)
internal class Pagination(val limit: Int, val offset: Int, val totalCount: Int)
internal data class Id(val id: String)

internal enum class SessionType {
Dial,
Bind
}
internal class SessionReq(val serviceId: String, val type: SessionType = SessionType.Dial)

data class ControllerVersion(val buildDate: String, val revision: String, val runtimeVersion: String, val version: String)
internal class Login(val username: String, val password: String)
internal class ApiSession(val id: String, val token: String, val identity: Identity?)

data class ServiceDNS(val hostname: String, val port: Int)
data class Service internal constructor(
class Service internal constructor(
val id: String, val name: String,
val encryptionRequired: Boolean,
internal val permissions: Set<SessionType>,

@SerializedName("postureQueries")
internal val postureSets: Array<PostureSet>?,

internal val config: Map<String,JsonObject>) {

val dns: ServiceDNS?
get() = getConfig(InterceptConfig, ServiceDNS::class.java)

fun <C> getConfig(configType: String, cls: Class<out C>): C? {
return Gson().fromJson(config[configType],cls)
fun <C> getConfig(configType: String, cls: Class<out C>): C? = Gson().fromJson(config[configType],cls)
}

data class PostureQueryProcess (
val osType: String,
val path: String,
)

data class PostureQuery (
val queryType: PostureQueryType,
val id: String,
val isPassing: Boolean,
val process: PostureQueryProcess?
)

internal data class PostureSet (
val isPassing: Boolean,
val policyId: String,
val postureQueries: Array<PostureQuery>?
)

@JsonAdapter(PostureResponseAdapter::class)
class PostureResponse (val id: String, val typeId: PostureQueryType, val data: Data) {
abstract class Data
class OS(val type: String, val version: String, val build: String) : Data()
class Domain(val domain: String): Data()
class MAC(val macAddresses: Array<String>): Data()

companion object {
fun OS(id: String, name: String, version: String, build: String) =
PostureResponse(id, PostureQueryType.OS, OS(name, version, build))
fun MAC(id: String, macs: Array<String>) =
PostureResponse(id, PostureQueryType.MAC, MAC(macs))
}
}

Expand Down Expand Up @@ -91,4 +140,25 @@ internal class CreateIdentity(val name: String, val type: String, enrollmentType
val enrollment = EnrollmentType(enrollmentType.equals("ott"))
}

internal const val InterceptConfig = "ziti-tunneler-client.v1"
class PostureResponseAdapter: TypeAdapter<PostureResponse>() {
override fun write(out: JsonWriter, value: PostureResponse) {
out.beginObject()
out.name("typeId")
out.value(value.typeId.name)
out.name("id")
out.value(value.id)

val dataTree = Gson().toJsonTree(value.data) as JsonObject
dataTree.entrySet().forEach {
out.name(it.key)
out.jsonValue(Gson().toJson(it.value))
}

out.endObject()
}

override fun read(`in`: JsonReader?): PostureResponse {
TODO("Not yet implemented")
}

}
Loading

0 comments on commit 1e5cc27

Please sign in to comment.