diff --git a/scanner_cli/.gitignore b/scanner_cli/.gitignore new file mode 100644 index 00000000..e65d3105 --- /dev/null +++ b/scanner_cli/.gitignore @@ -0,0 +1 @@ +dependencies/analysers/* diff --git a/scanner_cli/build.gradle.kts b/scanner_cli/build.gradle.kts index 457e4683..7d07e0ae 100644 --- a/scanner_cli/build.gradle.kts +++ b/scanner_cli/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2") // http client, on trial implementation("io.ktor:ktor-client-core:2.0.0") + implementation("ch.qos.logback:logback-classic:1.3.0-alpha14") // chapi domain implementation("com.phodal.chapi:chapi-domain:1.5.6") diff --git a/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/Runner.kt b/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/Runner.kt index 14bf2264..fd277200 100644 --- a/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/Runner.kt +++ b/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/Runner.kt @@ -22,8 +22,8 @@ class Runner { identifier = it.getValue("identifier"), host = it.getValue("host"), version = it.getValue("version"), - jar = it.getValue("jar"), className = it.getValue("className"), + jar = it.getValue("jar"), ) } ?: emptyList() } diff --git a/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/impl/CliSourceCodeContext.kt b/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/impl/CliSourceCodeContext.kt index d68dbe05..d3a1ec53 100644 --- a/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/impl/CliSourceCodeContext.kt +++ b/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/impl/CliSourceCodeContext.kt @@ -3,6 +3,7 @@ package org.archguard.scanner.ctl.impl import org.archguard.scanner.core.client.ArchGuardClient import org.archguard.scanner.core.sourcecode.SourceCodeContext +// extend SourceCodeContext to accept cil specific parameters(infra related), like memory limit, queue size, etc. class CliSourceCodeContext( val systemId: String, override val language: String, @@ -10,6 +11,4 @@ class CliSourceCodeContext( override val client: ArchGuardClient, override val path: String, override val withoutStorage: Boolean, -) : SourceCodeContext { - // extend SourceCodeContext to accept cil specific parameters(infra related), like memory limit, queue size, etc. -} +) : SourceCodeContext diff --git a/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/impl/OfficialAnalyserSpecs.kt b/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/impl/OfficialAnalyserSpecs.kt index 87b07818..ff14c1a2 100644 --- a/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/impl/OfficialAnalyserSpecs.kt +++ b/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/impl/OfficialAnalyserSpecs.kt @@ -2,18 +2,23 @@ package org.archguard.scanner.ctl.impl import org.archguard.scanner.core.AnalyserSpec -enum class OfficialAnalyserSpecs(val spec: AnalyserSpec) { - JAVA( - AnalyserSpec( - identifier = "java", - host = "https://github.com/archguard/scanner/releases/download/v1.5.0", - version = "1.5.0", - jar = "scan_sourcecode-1.5.0-all.jar", - className = "JavaAnalyser", - ) - ) +enum class OfficialAnalyserSpecs( + private val host: String, + private val version: String, + private val className: String, +) { + LANG_KOTLIN( + host = "https://github.com/archguard/scanner/tree/master/analyser_sourcecode/lang_kotlin/src/test/resources/kotlin", + version = "1.6.1", + className = "KotlinAnalyser", + ), ; + fun spec(): AnalyserSpec { + val identifier = name.lowercase() + return AnalyserSpec(identifier, host, version, "$identifier-$version-all.jar", className) + } + companion object { fun specs() = values().map(OfficialAnalyserSpecs::spec) } diff --git a/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/loader/AnalyserLoader.kt b/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/loader/AnalyserLoader.kt index 3aa2d82f..0a48b337 100644 --- a/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/loader/AnalyserLoader.kt +++ b/scanner_cli/src/main/kotlin/org/archguard/scanner/ctl/loader/AnalyserLoader.kt @@ -3,19 +3,70 @@ package org.archguard.scanner.ctl.loader import org.archguard.scanner.core.Analyser import org.archguard.scanner.core.AnalyserSpec import org.archguard.scanner.core.context.Context +import org.slf4j.LoggerFactory +import java.net.URLClassLoader +import java.nio.file.Path +import java.nio.file.Paths -// TODO load the scanner via classloader -// 扫描指定包/路径/类 object AnalyserLoader { + private val logger = LoggerFactory.getLogger(this.javaClass) + private val rootPath = Paths.get("").toAbsolutePath() + private const val folder = "dependencies/analysers" + + val installPath: Path = rootPath.resolve(folder) + + private fun AnalyserSpec.isInstalled(): Boolean { + logger.debug("workspace path: $rootPath") + logger.debug("analyser install path: $installPath") + + return (!installPath.toFile().listFiles { _, name -> name == jar }.isNullOrEmpty()).also { + if (it) logger.debug("analyser: $identifier - [$version] is installed") + else logger.debug("analyser: $identifier - [$version] is not installed") + } + } + + private fun AnalyserSpec.install() { + when { + // TODO fix windows + host.startsWith("/") -> this.copyFrom() + host.startsWith("http") -> this.download() + else -> throw IllegalArgumentException("please use absolute path or http url to install the analyser") + } + } + + private fun AnalyserSpec.download() { + TODO("download from remote url") + } + + private fun AnalyserSpec.copyFrom() { + val sourceJar = Paths.get(host).resolve(jar) + val targetJar = getLocalPath().toFile() + + logger.debug("analyser is configured as absolute path, copying to installation path...") + logger.debug("| $sourceJar -> $targetJar |") + + sourceJar.toFile().copyTo(targetJar) + } + + private fun AnalyserSpec.getFullClassName(): String { + if (className.contains(".")) return className + + return "org.archguard.scanner.analyser.$className" + } + + private fun AnalyserSpec.getLocalPath(): Path { + return rootPath.resolve(folder).resolve(jar) + } fun load(context: Context, spec: AnalyserSpec): Analyser { - // isInstalled - // install - // get with class for name - - // support full class name or short class name - return Class.forName("org.archguard.scanner.ctl." + spec.className) - .declaredConstructors[0] - .newInstance(context) as Analyser + if (!spec.isInstalled()) spec.install() + + return spec.run { + val jarUrl = getLocalPath().toUri().toURL() + val cl = URLClassLoader(arrayOf(jarUrl), this.javaClass.classLoader) + Class.forName(spec.getFullClassName(), true, cl) + .declaredConstructors[0] + .newInstance(context) as Analyser + } } } diff --git a/scanner_cli/src/test/kotlin/org/archguard/scanner/ctl/impl/OfficialAnalyserSpecsTest.kt b/scanner_cli/src/test/kotlin/org/archguard/scanner/ctl/impl/OfficialAnalyserSpecsTest.kt new file mode 100644 index 00000000..fddb039d --- /dev/null +++ b/scanner_cli/src/test/kotlin/org/archguard/scanner/ctl/impl/OfficialAnalyserSpecsTest.kt @@ -0,0 +1,15 @@ +package org.archguard.scanner.ctl.impl + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class OfficialAnalyserSpecsTest { + @Test + fun `should output the spec with the default jar for all the official analysers`() { + val specs = OfficialAnalyserSpecs.specs() + + assertThat(specs).allMatch { + it.jar == "${it.identifier}-${it.version}-all.jar" + } + } +} diff --git a/scanner_cli/src/test/kotlin/org/archguard/scanner/ctl/loader/AnalyserDispatcherTest.kt b/scanner_cli/src/test/kotlin/org/archguard/scanner/ctl/loader/AnalyserDispatcherTest.kt new file mode 100644 index 00000000..e50a6a67 --- /dev/null +++ b/scanner_cli/src/test/kotlin/org/archguard/scanner/ctl/loader/AnalyserDispatcherTest.kt @@ -0,0 +1,86 @@ +package org.archguard.scanner.ctl.loader + +import chapi.domain.core.CodeDataStruct +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.verify +import org.archguard.scanner.core.Analyser +import org.archguard.scanner.core.AnalyserSpec +import org.archguard.scanner.core.context.Context +import org.archguard.scanner.core.sourcecode.SourceCodeAnalyser +import org.archguard.scanner.core.sourcecode.SourceCodeContext +import org.archguard.scanner.ctl.impl.OfficialAnalyserSpecs +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class AnalyserDispatcherTest { + private val context = mockk { + every { language } returns "lang_kotlin" + every { features } returns listOf("feature1", "feature2") + } + + @BeforeEach + internal fun setUp() { + mockkObject(AnalyserLoader) + } + + @AfterEach + internal fun tearDown() { + clearAllMocks() + } + + @Test + fun `should dispatch to the official source code analyser, pass the ast to following feature analyser`() { + val customized = listOf( + mockk { every { identifier } returns "feature1" }, + mockk { every { identifier } returns "feature2" }, + ) + val languageAnalyser = mockk() + val feature1Analyser = mockk() + val feature2Analyser = mockk() + val ast = mockk>() + every { languageAnalyser.analyse(null) } returns ast + every { feature1Analyser.analyse(ast) } returns null + every { feature2Analyser.analyse(ast) } returns null + every { + AnalyserLoader.load(context, any()) + } returns (languageAnalyser as Analyser) andThen (feature1Analyser as Analyser) andThen (feature2Analyser as Analyser) + + val dispatcher = AnalyserDispatcher(context, customized) + dispatcher.dispatch() + + verify { + AnalyserLoader.load(context, OfficialAnalyserSpecs.LANG_KOTLIN.spec()) + } + } + + @Test + fun `should dispatch to the customized source code analyser`() { + val customizedLanguageAnalyser = mockk { every { identifier } returns "lang_kotlin" } + val customized = listOf( + customizedLanguageAnalyser, + mockk { every { identifier } returns "feature1" }, + mockk { every { identifier } returns "feature2" }, + ) + val languageAnalyser = mockk() + val feature1Analyser = mockk() + val feature2Analyser = mockk() + val ast = mockk>() + every { languageAnalyser.analyse(null) } returns ast + every { feature1Analyser.analyse(ast) } returns null + every { feature2Analyser.analyse(ast) } returns null + every { + AnalyserLoader.load(context, any()) + } returns (languageAnalyser as Analyser) andThen (feature1Analyser as Analyser) andThen (feature2Analyser as Analyser) + + val dispatcher = AnalyserDispatcher(context, customized) + dispatcher.dispatch() + + verify { + AnalyserLoader.load(context, customizedLanguageAnalyser) + } + } +} diff --git a/scanner_cli/src/test/kotlin/org/archguard/scanner/ctl/loader/AnalyserLoaderTest.kt b/scanner_cli/src/test/kotlin/org/archguard/scanner/ctl/loader/AnalyserLoaderTest.kt new file mode 100644 index 00000000..43677b80 --- /dev/null +++ b/scanner_cli/src/test/kotlin/org/archguard/scanner/ctl/loader/AnalyserLoaderTest.kt @@ -0,0 +1,62 @@ +package org.archguard.scanner.ctl.loader + +import io.mockk.every +import io.mockk.mockk +import org.archguard.scanner.core.AnalyserSpec +import org.archguard.scanner.core.sourcecode.SourceCodeContext +import org.archguard.scanner.ctl.loader.AnalyserLoader.installPath +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.io.File +import kotlin.io.path.Path +import kotlin.io.path.deleteIfExists +import kotlin.reflect.full.memberFunctions + +internal class AnalyserLoaderTest { + private val fakeJarName = "testonly-lang_kotlin-1.6.1-all.jar" + private val spec = AnalyserSpec( + identifier = "lang_kotlin", + host = "", + version = "1.6.1", + jar = fakeJarName, + className = "KotlinAnalyser", + ) + private val context = mockk { + every { client } returns mockk() + } + + private fun fakeInstall(source: File) = source.copyTo(installPath.resolve(spec.jar).toFile(), overwrite = true) + private fun fakeUninstall() = installPath.resolve(spec.jar).deleteIfExists() + + @BeforeEach + internal fun setUp() { + fakeUninstall() + } + + @Test + fun `should load the analyser from remote jar via http url`() { + // FIXME: implement this after upload the test jar + } + + @Test + fun `should load the analyser from local jar via absolute path`() { + val folder = this.javaClass.classLoader.getResource("kotlin")!! + val analyser = AnalyserLoader.load(context, spec.copy(host = folder.path)) + + val kClass = analyser::class + assertThat(kClass.simpleName).isEqualTo("KotlinAnalyser") + assertThat(kClass.memberFunctions.map { it.name }).contains("analyse") + } + + @Test + fun `should load the analyser from existing jar`() { + val folder = this.javaClass.classLoader.getResource("kotlin")!! + fakeInstall(Path(folder.path).resolve(fakeJarName).toFile()) + val analyser = AnalyserLoader.load(context, spec) + + val kClass = analyser::class + assertThat(kClass.simpleName).isEqualTo("KotlinAnalyser") + assertThat(kClass.memberFunctions.map { it.name }).contains("analyse") + } +} diff --git a/scanner_cli/src/test/resources/kotlin/lang_kotlin-1.6.1.jar b/scanner_cli/src/test/resources/kotlin/lang_kotlin-1.6.1.jar deleted file mode 100644 index 0f76778e..00000000 Binary files a/scanner_cli/src/test/resources/kotlin/lang_kotlin-1.6.1.jar and /dev/null differ diff --git a/scanner_cli/src/test/resources/kotlin/testonly-lang_kotlin-1.6.1-all.jar b/scanner_cli/src/test/resources/kotlin/testonly-lang_kotlin-1.6.1-all.jar new file mode 100644 index 00000000..424aa3a1 Binary files /dev/null and b/scanner_cli/src/test/resources/kotlin/testonly-lang_kotlin-1.6.1-all.jar differ diff --git a/scanner_core/src/main/kotlin/org/archguard/scanner/core/AnalyserSpec.kt b/scanner_core/src/main/kotlin/org/archguard/scanner/core/AnalyserSpec.kt index b6d12ccb..f6d62f07 100644 --- a/scanner_core/src/main/kotlin/org/archguard/scanner/core/AnalyserSpec.kt +++ b/scanner_core/src/main/kotlin/org/archguard/scanner/core/AnalyserSpec.kt @@ -7,4 +7,3 @@ data class AnalyserSpec( val jar: String, val className: String, // calculate via identifier?? ) -