diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/WebSecurityConfiguration.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/WebSecurityConfiguration.kt
index a7f2a346b3a3606cca9117b5a74fc30c458ea73b..f7db9de0d19bfa042a2dd783aa9a02aaaf85a1d1 100644
--- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/WebSecurityConfiguration.kt
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/WebSecurityConfiguration.kt
@@ -18,6 +18,10 @@ class SecurityConfiguration {
             authorizeExchange {
                 authorize("/gitlab/repoChanged", permitAll)
                 authorize("/metrics/latestRuns/{platform}/{id}", permitAll)
+                authorize("/kpi/{id}", permitAll)
+                authorize("/kpi/root/repository/{id}", permitAll)
+                authorize("/repository", permitAll)
+                authorize("/repository/{id}", permitAll)
             }
             csrf {
                 disable()
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/gitlab/GitlabService.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/gitlab/GitlabService.kt
index f4cad52a6be229d153f630053c9771a2d426e002..0ced35eebc9d8dbc3dd1adcc1d4a45580c1268cd 100644
--- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/gitlab/GitlabService.kt
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/gitlab/GitlabService.kt
@@ -12,7 +12,7 @@ class GitlabService(
 ) {
 
     suspend fun queryOpenCodeProject(repoId: Long) {
-        taskManager.addEvent(RepoChangedEvent(repoId = repoId, gitConfiguration = openCodeGitlabConfiguration))
+        taskManager.addEvent(RepoChangedEvent(repoId = repoId))
     }
 
 }
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/controller/KPIController.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/controller/KPIController.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1c44c24b3aa5dcd7f66d4796d588ec779a0c8f7e
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/controller/KPIController.kt
@@ -0,0 +1,35 @@
+package de.fraunhofer.iem.dataprovider.kpi.controller
+
+import de.fraunhofer.iem.dataprovider.kpi.dto.KPIResultDto
+import de.fraunhofer.iem.dataprovider.kpi.service.KPIService
+import de.fraunhofer.iem.dataprovider.logger.getLogger
+import de.fraunhofer.iem.dataprovider.metrics.*
+import de.fraunhofer.iem.dataprovider.toolRun.ToolRunService
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+import java.util.UUID
+
+@RestController
+@RequestMapping("/kpi")
+class KPIController(private val kpiService: KPIService, private val toolRunService: ToolRunService) {
+
+    private val logger = getLogger(javaClass)
+
+    @GetMapping("/{id}")
+    suspend fun getKPIByID(@PathVariable id: UUID): KPIResultDto {
+        logger.info("Get KPI with id $id")
+        val kpiEntity = this.kpiService.findKPIById(id) ?: throw Exception("KPI with id $id can not be found")
+        return kpiEntity.toResultDto()
+    }
+
+    // TODO: Change this to something meaningful
+    @GetMapping("/root/repository/{id}")
+    suspend fun getRootKPIByRepositoryId(@PathVariable id: UUID): KPIResultDto {
+        logger.info("Get root KPI from repository with id $id")
+        val repository = this.toolRunService.findRepoByID(id) ?: throw Exception("Repository can not be found")
+        val kpiEntity = this.kpiService.findRootKPIByRepository(repository) ?: throw Exception("Root KPI can not be found")
+        return kpiEntity.toResultDto()
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIDto.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIDto.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a8a6864d790350de2d82192c59695c017bf1c60f
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIDto.kt
@@ -0,0 +1,31 @@
+package de.fraunhofer.iem.dataprovider.kpi.dto
+
+import de.fraunhofer.iem.dataprovider.kpi.model.KPI
+import de.fraunhofer.iem.dataprovider.kpi.strategy.KPICalculationStrategy
+import java.sql.Timestamp
+import java.time.Instant
+
+
+class KPIDto(val name: String, val description: String, val isRoot: Boolean, private val calculationStrategy: KPICalculationStrategy) {
+    val hierarchyEdges: MutableList<KPIHierarchyEdgeDto> = mutableListOf<KPIHierarchyEdgeDto>()
+    var value: Int? = null
+
+    fun calculateKPI() {
+        this.value = calculationStrategy.calculateKPI(this.hierarchyEdges);
+    }
+
+    fun addChildKPI(kpiDto: KPIDto, weight: Double) {
+        val hierarchyEdge = KPIHierarchyEdgeDto(this, kpiDto, weight)
+        this.hierarchyEdges.add(hierarchyEdge)
+    }
+
+    fun toDbObject(): KPI {
+        val kpi = KPI()
+        kpi.name = this.name
+        kpi.value = this.value
+        kpi.description = this.description
+        kpi.isRoot = this.isRoot
+        kpi.timestamp = Timestamp.from(Instant.now())
+        return kpi
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIHierarchyEdgeDto.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIHierarchyEdgeDto.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e6170b9699d2c3442730cc6665ee4e85bd209c73
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIHierarchyEdgeDto.kt
@@ -0,0 +1,3 @@
+package de.fraunhofer.iem.dataprovider.kpi.dto
+
+data class KPIHierarchyEdgeDto (val from: KPIDto, val to: KPIDto, val weight: Double)
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIResultChildDto.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIResultChildDto.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1d68a4c7a6740c919fa17a31782987478f6f5b79
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIResultChildDto.kt
@@ -0,0 +1,5 @@
+package de.fraunhofer.iem.dataprovider.kpi.dto
+
+import java.util.*
+
+data class KPIResultChildDto(val id: UUID, val weight: Double)
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIResultDto.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIResultDto.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a8ddfd72e0c90df960b239b442e2d828a42dc28f
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIResultDto.kt
@@ -0,0 +1,5 @@
+package de.fraunhofer.iem.dataprovider.kpi.dto
+
+import java.util.*
+
+data class KPIResultDto(val id: UUID, val name: String, val description: String, val isRoot: Boolean, val children: List<KPIResultChildDto>, val repositoryId: UUID) {}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/model/KPI.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/model/KPI.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f53c1c27ec11eb9327f1ede4a2aea665fe15a3da
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/model/KPI.kt
@@ -0,0 +1,70 @@
+package de.fraunhofer.iem.dataprovider.kpi.model
+
+import de.fraunhofer.iem.dataprovider.kpi.dto.KPIResultChildDto
+import de.fraunhofer.iem.dataprovider.kpi.dto.KPIResultDto
+import de.fraunhofer.iem.dataprovider.toolRun.model.Repository
+import jakarta.persistence.*
+import org.hibernate.annotations.JdbcTypeCode
+import org.hibernate.type.SqlTypes
+import java.sql.Timestamp
+import java.util.*
+
+@Entity
+@Table(name = "kpi")
+class KPI {
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    @Column(name = "id", nullable = false)
+    var id: UUID? = null
+
+    @ManyToOne
+    @JoinColumn(name = "repository_id")
+    var repository: Repository? = null
+
+    @OneToMany(mappedBy = "from", cascade = [CascadeType.MERGE, CascadeType.REMOVE], orphanRemoval = true)
+    var hierarchyEdges: MutableSet<KPIHierarchyEdge> = mutableSetOf()
+
+    @Column(name = "value")
+    var value: Int? = null
+
+    @Column(name = "isRoot")
+    var isRoot: Boolean? = null
+
+    @Column(name = "name")
+    var name: String? = null
+
+    @Column(name = "description")
+    var description: String? = null
+
+    @JdbcTypeCode(SqlTypes.TIMESTAMP)
+    @Column(name = "timestamp")
+    var timestamp: Timestamp? = null
+
+    fun addHierarchyEdges(kpiHierarchyEdges: Collection<KPIHierarchyEdge>) {
+        kpiHierarchyEdges.forEach {
+            this.addHierarchyEdge(it)
+        }
+    }
+
+    fun addHierarchyEdge(hierarchyEdge: KPIHierarchyEdge) {
+        hierarchyEdge.from = this
+        this.hierarchyEdges.add(hierarchyEdge)
+    }
+
+    fun toResultDto(): KPIResultDto {
+        val childrenList = mutableListOf<KPIResultChildDto>()
+        for (edge in this.hierarchyEdges) {
+            val id = edge.to!!.id!!
+            val weight = edge.weight!!
+            childrenList.add(KPIResultChildDto(id, weight))
+        }
+        return KPIResultDto(
+                this.id!!,
+                this.name!!,
+                this.description!!,
+                this.isRoot!!,
+                childrenList,
+                this.repository!!.id!!
+        )
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/model/KPIHierarchyEdge.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/model/KPIHierarchyEdge.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6994560f93a2b26b438c77aec9e3aa5c1ea88bd9
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/model/KPIHierarchyEdge.kt
@@ -0,0 +1,24 @@
+package de.fraunhofer.iem.dataprovider.kpi.model
+
+import jakarta.persistence.*
+import java.util.*
+
+@Entity
+@Table(name = "kpi_hierarchy_edge")
+class KPIHierarchyEdge {
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    @Column(name = "id", nullable = false)
+    var id: UUID? = null
+
+    @ManyToOne(cascade = [CascadeType.MERGE])
+    @JoinColumn(name = "from_id")
+    var from: KPI? = null
+
+    @ManyToOne(cascade = [CascadeType.MERGE])
+    @JoinColumn(name = "to_id")
+    var to: KPI? = null
+
+    @Column(name = "weight")
+    var weight: Double? = null
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/repository/KPIHierarchyEdgeRepository.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/repository/KPIHierarchyEdgeRepository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5d455ef31b09b1b26aa62cf7baf8da69ecd64166
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/repository/KPIHierarchyEdgeRepository.kt
@@ -0,0 +1,7 @@
+package de.fraunhofer.iem.dataprovider.kpi.repository
+
+import de.fraunhofer.iem.dataprovider.kpi.model.KPIHierarchyEdge
+import org.springframework.data.jpa.repository.JpaRepository
+import java.util.*
+
+interface KPIHierarchyEdgeRepository: JpaRepository<KPIHierarchyEdge, UUID> {}
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/repository/KPIRepository.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/repository/KPIRepository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3967d7caab516def4a6be42cb894c474dc620715
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/repository/KPIRepository.kt
@@ -0,0 +1,11 @@
+package de.fraunhofer.iem.dataprovider.kpi.repository
+
+import de.fraunhofer.iem.dataprovider.kpi.model.KPI
+import org.springframework.data.jpa.repository.JpaRepository
+import java.util.*
+
+interface KPIRepository : JpaRepository<KPI, UUID> {
+    fun findByRepository_Id(id: UUID): List<KPI>
+
+    fun findFirstByRepository_IdAndIsRootTrue(id: UUID): Optional<KPI>
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/service/KPIService.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/service/KPIService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e6ab6b0f3deb6d8b3398e12ab32e316bac0448c3
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/service/KPIService.kt
@@ -0,0 +1,106 @@
+package de.fraunhofer.iem.dataprovider.kpi.service
+
+import de.fraunhofer.iem.dataprovider.kpi.dto.KPIDto
+import de.fraunhofer.iem.dataprovider.kpi.model.KPI
+import de.fraunhofer.iem.dataprovider.kpi.model.KPIHierarchyEdge
+import de.fraunhofer.iem.dataprovider.kpi.repository.KPIHierarchyEdgeRepository
+import de.fraunhofer.iem.dataprovider.kpi.repository.KPIRepository
+import de.fraunhofer.iem.dataprovider.logger.getLogger
+import de.fraunhofer.iem.dataprovider.toolRun.model.Repository
+import org.springframework.data.repository.findByIdOrNull
+import org.springframework.stereotype.Service
+import java.util.*
+
+@Service
+class KPIService(private val kpiRepository: KPIRepository, private val kpiHierarchyEdgeRepository: KPIHierarchyEdgeRepository) {
+
+    private val logger = getLogger(javaClass)
+
+    private fun calculateKPIsRecursively(kpi: KPIDto, visited: MutableSet<KPIDto>) {
+        // Check if the KPI has already been visited
+        if (visited.contains(kpi)) {
+            return
+        }
+
+        // Check if the KPI has child KPIs
+        if (kpi.hierarchyEdges.isEmpty()) {
+            // Leaf node, calculate the KPI value
+            kpi.calculateKPI()
+            visited.add(kpi)
+            return
+        }
+
+        // Recursively calculate child KPIs first
+        for (childEdge in kpi.hierarchyEdges) {
+            calculateKPIsRecursively(childEdge.to, visited)
+        }
+
+        // Calculate the KPI value after processing child KPIs
+        kpi.calculateKPI()
+        visited.add(kpi)
+    }
+
+    fun calculateKPIs(rootKPI: KPIDto) {
+        val visited: MutableSet<KPIDto> = mutableSetOf()
+        calculateKPIsRecursively(rootKPI, visited)
+    }
+
+    fun storeAndPurgeOld(repository: Repository, rootKPI: KPIDto) {
+        purgeAllKPIs(repository)
+        val kpiDtoToEntityMapping: MutableMap<KPIDto, KPI> = mutableMapOf()
+        storeKPI(repository, rootKPI, kpiDtoToEntityMapping)
+    }
+
+    private fun storeKPI(repository: Repository, kpi: KPIDto, kpiDtoToEntityMapping: MutableMap<KPIDto, KPI>) {
+        // already visited
+        if (kpiDtoToEntityMapping.contains(kpi)) {
+            return
+        }
+
+        if (kpi.hierarchyEdges.isEmpty()) {
+            // leaf node
+            val kpiEntity = kpi.toDbObject()
+            kpiEntity.repository = repository
+
+            kpiDtoToEntityMapping[kpi] = kpiEntity
+            kpiRepository.save(kpiEntity)
+            logger.info("Storing leaf node ${kpi.name}")
+            return
+        }
+
+        for (childEdge in kpi.hierarchyEdges) {
+            storeKPI(repository, childEdge.to, kpiDtoToEntityMapping)
+        }
+
+        val kpiEntity = kpi.toDbObject()
+        kpiEntity.repository = repository
+        kpiDtoToEntityMapping[kpi] = kpiEntity
+        kpiRepository.save(kpiEntity)
+        logger.info("Storing node ${kpi.name}")
+        for (hierarchyEdge in kpi.hierarchyEdges) {
+            val hierarchyEdgeEntity = KPIHierarchyEdge()
+            hierarchyEdgeEntity.from = kpiEntity
+            hierarchyEdgeEntity.to = kpiDtoToEntityMapping[hierarchyEdge.to]
+            hierarchyEdgeEntity.weight = hierarchyEdge.weight
+            kpiHierarchyEdgeRepository.save(hierarchyEdgeEntity)
+            logger.info("Storing edge ${hierarchyEdge.weight}: From ${hierarchyEdge.from.name} to ${hierarchyEdge.to.name}")
+        }
+    }
+
+    private fun purgeAllKPIs(repository: Repository) {
+        val kpis: List<KPI> = kpiRepository.findByRepository_Id(repository.id!!)
+        kpiRepository.deleteAll(kpis)
+    }
+
+    fun findKPIById(id: UUID): KPI? {
+        return kpiRepository.findByIdOrNull(id)
+    }
+
+    fun findRootKPIByRepository(repository: Repository): KPI? {
+        val kpi = kpiRepository.findFirstByRepository_IdAndIsRootTrue(repository.id!!)
+        if (kpi.isEmpty) {
+            return null
+        }
+        return kpi.get()
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/AggregationKPICalculationStrategy.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/AggregationKPICalculationStrategy.kt
new file mode 100644
index 0000000000000000000000000000000000000000..394778a6feb7c2fe2014292f2e1caa098395628d
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/AggregationKPICalculationStrategy.kt
@@ -0,0 +1,16 @@
+package de.fraunhofer.iem.dataprovider.kpi.strategy
+
+import de.fraunhofer.iem.dataprovider.kpi.dto.KPIHierarchyEdgeDto
+
+class AggregationKPICalculationStrategy: KPICalculationStrategy {
+    override fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Int {
+        var aggregate = 0;
+        for (child in children) {
+            if (child.to.value != null) {
+                val childValue = child.to.value!!;
+                aggregate += (childValue.toFloat() * child.weight).toInt();
+            }
+        }
+        return aggregate;
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/KPICalculationStrategy.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/KPICalculationStrategy.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1545b7f3a2c1cafb5ea29e1c7239be64243abeec
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/KPICalculationStrategy.kt
@@ -0,0 +1,8 @@
+package de.fraunhofer.iem.dataprovider.kpi.strategy
+
+import de.fraunhofer.iem.dataprovider.kpi.dto.KPIHierarchyEdgeDto
+
+interface KPICalculationStrategy {
+    fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Int;
+}
+
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/RatioKPICalculationStrategy.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/RatioKPICalculationStrategy.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ae66607cd4aa1b962fcfef71dfe21c730ff92bda
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/RatioKPICalculationStrategy.kt
@@ -0,0 +1,17 @@
+package de.fraunhofer.iem.dataprovider.kpi.strategy
+
+import de.fraunhofer.iem.dataprovider.kpi.dto.KPIHierarchyEdgeDto
+
+class RatioKPICalculationStrategy(): KPICalculationStrategy {
+    override fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Int {
+        if (children.size != 2) {
+            throw Exception("Requires exactly two children")
+        }
+        val firstValue = children[0].to.value
+        val secondValue = children[1].to.value
+        if (firstValue!! >= secondValue!!) {
+            return ((secondValue / firstValue) * 100).toInt();
+        }
+        return ((firstValue / secondValue) * 100).toInt();
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/RawValueKPICalculationStrategy.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/RawValueKPICalculationStrategy.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d81b0fbcd6d4dbf2301c483616ed2b564826a62a
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/RawValueKPICalculationStrategy.kt
@@ -0,0 +1,9 @@
+package de.fraunhofer.iem.dataprovider.kpi.strategy
+
+import de.fraunhofer.iem.dataprovider.kpi.dto.KPIHierarchyEdgeDto
+
+class RawValueKPICalculationStrategy(val value: Int): KPICalculationStrategy {
+    override fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Int {
+        return this.value;
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/metrics/MetricsController.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/metrics/MetricsController.kt
index a9926f043b6cb3b73277ae070cd0ed0217576bdc..de94d4a75d6a8659dc3c5807324df03bc9ec46bc 100644
--- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/metrics/MetricsController.kt
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/metrics/MetricsController.kt
@@ -35,6 +35,5 @@ class MetricsController(private val toolRunService: ToolRunService) {
             )
         }
         return ToolResultsDto(repoDto, trDto)
-
     }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/repository/controller/RepositoryController.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/repository/controller/RepositoryController.kt
new file mode 100644
index 0000000000000000000000000000000000000000..42b68c67410b9c509bee2d593b895319241b80e2
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/repository/controller/RepositoryController.kt
@@ -0,0 +1,32 @@
+package de.fraunhofer.iem.dataprovider.repository.controller
+
+import de.fraunhofer.iem.dataprovider.logger.getLogger
+import de.fraunhofer.iem.dataprovider.metrics.*
+import de.fraunhofer.iem.dataprovider.repository.dto.RepositoryDto
+import de.fraunhofer.iem.dataprovider.toolRun.ToolRunService
+import de.fraunhofer.iem.dataprovider.toolRun.model.Repository
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RestController
+import java.util.UUID
+
+@RestController
+@RequestMapping("/repository")
+class RepositoryController(private val toolRunService: ToolRunService) {
+
+    private val logger = getLogger(javaClass)
+
+    @GetMapping()
+    suspend fun getAllRepositories(): List<RepositoryDto> {
+        logger.info("Get all repositories")
+        return toolRunService.getAllRepositories().map { repository: Repository -> RepositoryDto(repository.id!!, repository.name!!) }
+    }
+
+    @GetMapping("/{id}")
+    suspend fun getRepositoryById(@PathVariable id: UUID): RepositoryDto {
+        logger.info("Get repository with id $id")
+        val repositoryEntity = this.toolRunService.findRepoByID(id) ?: throw Exception("Repository can not be found")
+        return RepositoryDto(repositoryEntity.id!!, repositoryEntity.name!!)
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/repository/dto/RepositoryDto.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/repository/dto/RepositoryDto.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b01fef603dd5a030224f12033f4477646dd8aa72
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/repository/dto/RepositoryDto.kt
@@ -0,0 +1,5 @@
+package de.fraunhofer.iem.dataprovider.repository.dto
+
+import java.util.*
+
+data class RepositoryDto(val id: UUID, val name: String){}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/TaskManager.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/TaskManager.kt
index 41507f32b9642c0befadc9de0cf60f1012e515f3..aab943f1ac4ca43f926012f53b2cd38d74902fb9 100644
--- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/TaskManager.kt
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/TaskManager.kt
@@ -1,5 +1,7 @@
 package de.fraunhofer.iem.dataprovider.taskManager
 
+import de.fraunhofer.iem.dataprovider.gitlab.OpenCodeGitlabConfiguration
+import de.fraunhofer.iem.dataprovider.kpi.service.KPIService
 import de.fraunhofer.iem.dataprovider.logger.getLogger
 import de.fraunhofer.iem.dataprovider.taskManager.model.*
 import de.fraunhofer.iem.dataprovider.taskManager.tasks.*
@@ -24,8 +26,10 @@ import java.util.*
  */
 @Component
 class TaskManager(
-    private val config: Config,
-    private val toolRunService: ToolRunService
+        private val config: Config,
+        private val openCodeGitlabConfiguration: OpenCodeGitlabConfiguration,
+        private val toolRunService: ToolRunService,
+        private val kpiService: KPIService
 ) {
 
     // The used default dispatcher is ok for CPU-bound workloads. However,
@@ -81,7 +85,7 @@ class TaskManager(
                     ioWorker.addTask(
                         GetGitlabProjectTask(
                             event.repoId,
-                            event.gitConfiguration,
+                            openCodeGitlabConfiguration,
                             ::addEvent,
                             toolRunService
                         )
@@ -145,16 +149,20 @@ class TaskManager(
                 }
 
                 is SarifProcessGroupDone -> {
+                    logger.info("Done with Sarif Task")
                     val eventGroup = dependentEvents[event.groupId]
-
                     if (eventGroup != null) {
                         eventGroup.remove(event.taskId)
                         if (eventGroup.isEmpty()) {
-                            worker.addTask(MetricsTask(event.repoId, toolRunService, ::addEvent))
+                            logger.info("Adding repository details task")
+                            worker.addTask(GetRepositoryDetailsTask(event.repoId, openCodeGitlabConfiguration, toolRunService, ::addEvent))
                         }
                     }
                 }
 
+                is GetRepositoryDetailsDone -> {
+                    worker.addTask(MetricsTask(event.repoId, toolRunService, kpiService, ::addEvent))
+                }
 
                 else -> {
                     logger.info("Received event without special handling associated $event")
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/model/Event.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/model/Event.kt
index 7d3eaade44f873907fa74c3dfe4f41e4a2c675cc..c05208f79850642cc03a79bbe60aac0b2d4642ae 100644
--- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/model/Event.kt
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/model/Event.kt
@@ -1,12 +1,12 @@
 package de.fraunhofer.iem.dataprovider.taskManager.model
 
-import de.fraunhofer.iem.dataprovider.gitlab.GitConfiguration
 import de.fraunhofer.iem.dataprovider.taskManager.tasks.GitRepository
+import de.fraunhofer.iem.dataprovider.toolRun.model.RepositoryDetails
 import de.fraunhofer.iem.dataprovider.toolRun.model.Sarif
 import java.util.*
 
 sealed class Event
-class RepoChangedEvent(val gitConfiguration: GitConfiguration, val repoId: Long) : Event()
+class RepoChangedEvent(val repoId: Long) : Event()
 
 sealed class TaskDone : Event() {
     abstract val taskId: UUID
@@ -18,3 +18,5 @@ class GetGitlabProjectDone(override val taskId: UUID, val repoId: UUID, val gitR
 class SarifProcessDone(override val taskId: UUID, val repoId: UUID, val sarif: Sarif) : TaskDone()
 class SarifProcessGroupDone(override val taskId: UUID, val repoId: UUID, val groupId: UUID, val sarif: Sarif) :
     TaskDone()
+
+class GetRepositoryDetailsDone(override val taskId: UUID, val repoId: UUID, val repositoryDetailsEntity: RepositoryDetails) : TaskDone()
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/GetRepositoryDetailsTask.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/GetRepositoryDetailsTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e85c940c84fa49339dff39671f65865e7f539e57
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/GetRepositoryDetailsTask.kt
@@ -0,0 +1,65 @@
+package de.fraunhofer.iem.dataprovider.taskManager.tasks
+
+import de.fraunhofer.iem.dataprovider.gitlab.GitConfiguration
+import de.fraunhofer.iem.dataprovider.taskManager.model.Event
+import de.fraunhofer.iem.dataprovider.taskManager.model.GetRepositoryDetailsDone
+import de.fraunhofer.iem.dataprovider.toolRun.ToolRunService
+import de.fraunhofer.iem.dataprovider.toolRun.model.Repository
+import org.gitlab4j.api.GitLabApi
+import org.gitlab4j.api.models.Commit
+import org.gitlab4j.api.models.Project
+import java.util.UUID
+
+class GetRepositoryDetailsTask(
+        private val repoId: UUID,
+        private val gitlabConfiguration: GitConfiguration,
+        private val toolRunService: ToolRunService,
+        override val responseChannel: suspend (event: Event) -> Unit,
+) : Task() {
+    private val gitlabApi: GitLabApi = GitLabApi(gitlabConfiguration.host, gitlabConfiguration.accessToken)
+
+    override suspend fun execute() {
+        val repositoryEntity = toolRunService.findRepoByID(repoId);
+        logger.info(repositoryEntity.toString())
+        if (repositoryEntity != null) {
+            val project = gitlabApi.projectApi.getProject(repositoryEntity.repoId)
+            // Note: We only take commits from the default branch
+            val commits = gitlabApi.commitsApi.getCommits(project.id, project.defaultBranch, ".")
+
+            val numberOfCommits = commits.count()
+            val numberOfSignedCommits = getNumberOfSignedCommits(repositoryEntity, commits)
+            val isDefaultBranchProtected = isDefaultBranchProtected(repositoryEntity, project)
+            val repositoryDetailsDto = RepositoryDetailsDto(repositoryEntity, numberOfCommits, numberOfSignedCommits, isDefaultBranchProtected)
+            logger.info("Collected repository details from $repoId (${repositoryDetailsDto}) successfully")
+            val repositoryDetailsEntity = toolRunService.createRepositoryDetails(repositoryDetailsDto);
+            logger.info("Stored repository details for $repoId (${repositoryDetailsEntity}) successfully")
+            responseChannel(GetRepositoryDetailsDone(taskID, repoId, repositoryDetailsEntity))
+        } else {
+            logger.error("Repository $repoId can not be found in the database")
+        }
+    }
+
+    // TODO: This should probably live somewhere else and encapsulate the logic
+    private fun getNumberOfSignedCommits(repositoryEntity: Repository, commits: List<Commit>): Int {
+        var numberOfSignedCommits = 0
+        for (commit in commits) {
+            val signature = gitlabApi.commitsApi.getOptionalGpgSignature(repositoryEntity.repoId, commit.id)
+            if (signature !== null) {
+                numberOfSignedCommits++
+            }
+        }
+        return numberOfSignedCommits
+    }
+
+    // TODO: This should probably live somewhere else and encapsulate the logic
+    private fun isDefaultBranchProtected(repositoryEntity: Repository, project: Project): Boolean {
+        return try {
+            val defaultBranchName = project.defaultBranch;
+            val branch = gitlabApi.repositoryApi.getBranch(repositoryEntity.repoId, defaultBranchName);
+            branch.protected
+        } catch (e: Exception) {
+            // in theory, error probably happens if branch can't be found. In this case we default to false
+            false;
+        }
+    }
+}
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/MetricsTask.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/MetricsTask.kt
index 9b583dd61c0524d562daa948e62796aaaaf32c82..6d75ef756444d157d37a5cfc0e1ba9283f234756 100644
--- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/MetricsTask.kt
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/MetricsTask.kt
@@ -1,17 +1,52 @@
 package de.fraunhofer.iem.dataprovider.taskManager.tasks
 
+import de.fraunhofer.iem.dataprovider.kpi.dto.KPIDto
+import de.fraunhofer.iem.dataprovider.kpi.service.KPIService
 import de.fraunhofer.iem.dataprovider.taskManager.model.Event
+import de.fraunhofer.iem.dataprovider.kpi.strategy.AggregationKPICalculationStrategy
+import de.fraunhofer.iem.dataprovider.kpi.strategy.RatioKPICalculationStrategy
+import de.fraunhofer.iem.dataprovider.kpi.strategy.RawValueKPICalculationStrategy
 import de.fraunhofer.iem.dataprovider.toolRun.ToolRunService
+import de.fraunhofer.iem.dataprovider.toolRun.model.RepositoryDetails
 import java.util.*
 
 class MetricsTask(
     val repoId: UUID,
     private val toolRunService: ToolRunService,
-
+    private val kpiService: KPIService,
     override val responseChannel: suspend (task: Event) -> Unit
 ) : Task() {
     override suspend fun execute() {
         logger.info("Starting metrics task for repoId $repoId")
-        toolRunService.getLatestToolRunsForRepo(repoId)
+        val repository = toolRunService.findRepoByID(repoId);
+        val repositoryDetails = toolRunService.getLatestRepositoryDetailsByRepositoryId(repoId);
+        if (repositoryDetails != null && repository != null) {
+            val rootKPI = generateKPITree(repositoryDetails)
+            kpiService.calculateKPIs(rootKPI)
+            kpiService.storeAndPurgeOld(repository, rootKPI)
+        }
+    }
+
+    private fun generateKPITree(repositoryDetails: RepositoryDetails): KPIDto {
+        // lowest level leaves
+        val numberOfCommitsKPI = KPIDto( "Number of commits", "Overall transparency score between 0-100 :-)", false, RawValueKPICalculationStrategy(repositoryDetails.numberOfCommits!!))
+        val numberOfSignedCommitsKPI = KPIDto("Number of signed commits", "Overall transparency score between 0-100 :-)", false, RawValueKPICalculationStrategy(repositoryDetails.numberOfSignedCommits!!))
+        val isDefaultBranchProtectedKPI = KPIDto( "Is Default Branch Protected", "Is the default branch protected?", false, RawValueKPICalculationStrategy(if (repositoryDetails.isDefaultBranchProtected == true) 100 else 0))
+
+        val signedCommitsRatioKPI = KPIDto( "Signed Commit Ratio", "Ratio between signed and all commits", false, RatioKPICalculationStrategy())
+        signedCommitsRatioKPI.addChildKPI(numberOfCommitsKPI, 1.0)
+        signedCommitsRatioKPI.addChildKPI(numberOfSignedCommitsKPI, 1.0)
+
+        // second level
+        val securityKPI = KPIDto(  "Security score", "Overall project security score between 0-100 :-)", false, AggregationKPICalculationStrategy())
+        val transparencyKPI = KPIDto( "Transparency score", "Overall transparency score between 0-100 :-)", false, AggregationKPICalculationStrategy())
+        securityKPI.addChildKPI(isDefaultBranchProtectedKPI, 0.5)
+        securityKPI.addChildKPI(signedCommitsRatioKPI, 0.5)
+        transparencyKPI.addChildKPI(signedCommitsRatioKPI, 1.0)
+
+        val rootKPI = KPIDto( "Project score", "Overall project score between 0-100 :-)", true, AggregationKPICalculationStrategy())
+        rootKPI.addChildKPI(securityKPI, 0.5)
+        rootKPI.addChildKPI(transparencyKPI, 0.5)
+        return rootKPI
     }
 }
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/RepositoryDetailsDto.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/RepositoryDetailsDto.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4c24fec643f567a9ae83cf37171e001ddbee389a
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/RepositoryDetailsDto.kt
@@ -0,0 +1,18 @@
+package de.fraunhofer.iem.dataprovider.taskManager.tasks
+
+import de.fraunhofer.iem.dataprovider.toolRun.model.Repository
+import de.fraunhofer.iem.dataprovider.toolRun.model.RepositoryDetails
+import java.sql.Timestamp
+import java.time.Instant
+
+data class RepositoryDetailsDto(val repository: Repository, val numberOfCommits: Int, val numberOfSignedCommits: Int, val isDefaultBranchProtected: Boolean)
+
+fun RepositoryDetailsDto.toDbObject(): RepositoryDetails {
+    val repositoryDetails = RepositoryDetails()
+    repositoryDetails.repository = this.repository
+    repositoryDetails.timestamp = Timestamp.from(Instant.now())
+    repositoryDetails.numberOfCommits = this.numberOfCommits
+    repositoryDetails.numberOfSignedCommits = this.numberOfSignedCommits
+    repositoryDetails.isDefaultBranchProtected = this.isDefaultBranchProtected
+    return repositoryDetails
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/RepositoryDetailsRepository.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/RepositoryDetailsRepository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a13b4997cbe95c8202fb0487aeaf505544167548
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/RepositoryDetailsRepository.kt
@@ -0,0 +1,11 @@
+package de.fraunhofer.iem.dataprovider.toolRun
+
+import de.fraunhofer.iem.dataprovider.toolRun.model.RepositoryDetails
+import org.springframework.data.domain.Sort
+import org.springframework.data.jpa.repository.JpaRepository
+import java.util.*
+
+
+interface RepositoryDetailsRepository : JpaRepository<RepositoryDetails, UUID> {
+    fun findFirstByRepository_IdOrderByTimestampDesc(id: UUID): Optional<RepositoryDetails>
+}
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/ToolRunService.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/ToolRunService.kt
index 2e8808b9dadf4c2dec73681937ee12580cb79399..086578a4474204ed9be25f2ac0e878d10e3cddd0 100644
--- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/ToolRunService.kt
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/ToolRunService.kt
@@ -3,6 +3,7 @@ package de.fraunhofer.iem.dataprovider.toolRun
 import de.fraunhofer.iem.dataprovider.gitlab.Platform
 import de.fraunhofer.iem.dataprovider.logger.getLogger
 import de.fraunhofer.iem.dataprovider.taskManager.tasks.GitRepository
+import de.fraunhofer.iem.dataprovider.taskManager.tasks.RepositoryDetailsDto
 import de.fraunhofer.iem.dataprovider.taskManager.tasks.toDbObject
 import de.fraunhofer.iem.dataprovider.toolRun.model.*
 import org.springframework.stereotype.Service
@@ -11,6 +12,7 @@ import java.util.*
 @Service
 class ToolRunService(
     private val repositoryRepository: RepositoryRepository,
+    private val repositoryDetailsRepository: RepositoryDetailsRepository,
     private val toolRunRepository: ToolRunRepository,
     private val toolRepository: ToolRepository,
     private val ruleRepository: RuleRepository
@@ -121,4 +123,38 @@ class ToolRunService(
         }
         return repo
     }
+
+    // TODO: We should decide if we want to return optionals (null if not found) or throw exceptions or ...
+    // TODO: Must be consistent across all methods
+    fun findRepoByID(id: UUID): Repository? {
+        val repository = repositoryRepository.findById(id)
+        if (repository.isEmpty) {
+            return null
+        }
+        return repository.get()
+    }
+
+    fun createRepositoryDetails(repositoryDetailsDto: RepositoryDetailsDto): RepositoryDetails {
+        return repositoryDetailsRepository.save(repositoryDetailsDto.toDbObject())
+    }
+
+    fun getLatestRepositoryDetailsByRepositoryId(repositoryId: UUID): RepositoryDetails? {
+        val repositoryDetails = repositoryDetailsRepository.findFirstByRepository_IdOrderByTimestampDesc(repositoryId);
+        if (repositoryDetails.isEmpty) {
+            return null
+        }
+        return repositoryDetails.get();
+    }
+
+    fun getRepositoryDetailsById(id: UUID): RepositoryDetails? {
+        val repositoryDetails = repositoryDetailsRepository.findById(id);
+        if (repositoryDetails.isEmpty) {
+            return null
+        }
+        return repositoryDetails.get()
+    }
+
+    fun getAllRepositories(): List<Repository> {
+        return repositoryRepository.findAll()
+    }
 }
diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/model/RepositoryDetails.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/model/RepositoryDetails.kt
new file mode 100644
index 0000000000000000000000000000000000000000..52a0fe37ae638b4f68f413c49d544c46a369470b
--- /dev/null
+++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/model/RepositoryDetails.kt
@@ -0,0 +1,32 @@
+package de.fraunhofer.iem.dataprovider.toolRun.model
+
+import jakarta.persistence.*
+import org.hibernate.annotations.JdbcTypeCode
+import org.hibernate.type.SqlTypes
+import java.sql.Timestamp
+import java.util.*
+
+@Entity
+@Table(name = "repository_details")
+class RepositoryDetails {
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    @Column(name = "id", nullable = false)
+    var id: UUID? = null
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    var repository: Repository? = null
+
+    @Column(name = "number_of_commits")
+    var numberOfCommits: Int? = null
+
+    @Column(name = "number_of_signed_commits")
+    var numberOfSignedCommits: Int? = null
+
+    @Column(name = "is_default_branch_protected")
+    var isDefaultBranchProtected: Boolean? = null
+
+    @JdbcTypeCode(SqlTypes.TIMESTAMP)
+    @Column(name = "timestamp")
+    var timestamp: Timestamp? = null
+}
\ No newline at end of file