Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

## [Unreleased]

## [1.6.0] - 2024-03-XX

- Add Infrastructure as Code (IaC) support
- Add icons for file nodes in the tree view
- Add a full path of file nodes in the tree view

## [1.5.0] - 2024-03-13

- Add SCA Violation Card
Expand Down Expand Up @@ -62,6 +68,8 @@

The first public release of the plugin.

[1.6.0]: https://github.com/cycodehq/intellij-platform-plugin/releases/tag/v1.6.0

[1.5.0]: https://github.com/cycodehq/intellij-platform-plugin/releases/tag/v1.5.0

[1.4.0]: https://github.com/cycodehq/intellij-platform-plugin/releases/tag/v1.4.0
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The extension provides functionalities such as:

* Scanning your code for exposed secrets, passwords, tokens, keys, and other credentials.
* Scanning your code for open-source package`s vulnerabilities.
* Scanning your code for Infrastructure as Code (IaC).
* Company’s Custom Remediation Guidelines - If your company has set custom remediation guidelines via the Cycode portal, you'll see a field for "Company Guidelines" that contains those guidelines.
* Running a new scan from your IDE even before committing the code.
* Triggering a scan automatically whenever a file is saved.
Expand All @@ -22,7 +23,7 @@ The extension provides functionalities such as:
* Removing a detected secret or ignoring it by secret value, rule (type) or by path.
* Upgrading a detected vulnerable package or ignoring it by rule (type) or by path.

Coming soon: Code Security (SAST), and Infrastructure as Code (IaC).
Coming soon: Code Security (SAST).

## Installation

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pluginGroup = com.cycode.plugin
pluginName = Cycode
pluginRepositoryUrl = https://github.com/cycodehq/intellij-platform-plugin
# SemVer format -> https://semver.org
pluginVersion = 1.5.0
pluginVersion = 1.6.0

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 211.1
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/cycode/plugin/Consts.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Consts {
companion object {
val PLUGIN_PATH = PathManager.getPluginsPath() + "/cycode-intellij-platform-plugin"
val DEFAULT_CLI_PATH = getDefaultCliPath()
const val REQUIRED_CLI_VERSION = "1.9.1"
const val REQUIRED_CLI_VERSION = "1.9.2"

const val CLI_GITHUB_ORG = "cycodehq"
const val CLI_GITHUB_REPO = "cycode-cli"
Expand Down
214 changes: 9 additions & 205 deletions src/main/kotlin/com/cycode/plugin/annotators/CycodeAnnotator.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
package com.cycode.plugin.annotators

import com.cycode.plugin.CycodeBundle
import com.cycode.plugin.cli.CliResult
import com.cycode.plugin.cli.CliScanType
import com.cycode.plugin.cli.getPackageFileForLockFile
import com.cycode.plugin.cli.isSupportedLockFile
import com.cycode.plugin.intentions.CycodeIgnoreIntentionQuickFix
import com.cycode.plugin.intentions.CycodeIgnoreType
import com.cycode.plugin.annotators.annotationAppliers.IacApplier
import com.cycode.plugin.annotators.annotationAppliers.ScaApplier
import com.cycode.plugin.annotators.annotationAppliers.SecretApplier
import com.cycode.plugin.services.ScanResultsService
import com.cycode.plugin.services.scanResults
import com.intellij.lang.ExternalLanguageAnnotators
import com.intellij.lang.Language
import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.lang.annotation.ExternalAnnotator
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile

private val LOG = logger<CycodeAnnotator>()
Expand Down Expand Up @@ -123,208 +117,18 @@ class CycodeAnnotator : DumbAware, ExternalAnnotator<PsiFile, Unit>() {

applyAnnotationsForSecrets(psiFile, holder)
applyAnnotationsForSca(psiFile, holder)
}

private fun convertSeverity(severity: String): HighlightSeverity {
return when (severity.toLowerCase()) {
"critical" -> HighlightSeverity.ERROR
"high" -> HighlightSeverity.ERROR
"medium" -> HighlightSeverity.WARNING
"low" -> HighlightSeverity.WEAK_WARNING
else -> HighlightSeverity.INFORMATION
}
}

private fun validateSecretTextRange(textRange: TextRange, psiFile: PsiFile): Boolean {
val scanResults = getScanResults(psiFile)
val detectedSubstr = psiFile.text.substring(textRange.startOffset, textRange.endOffset)
val detectedSegment = scanResults.getDetectedSegment(CliScanType.Secret, textRange)
if (detectedSegment == null) {
scanResults.saveDetectedSegment(CliScanType.Secret, textRange, detectedSubstr)
} else if (detectedSegment != detectedSubstr) {
// case: the code has been added or deleted before the detection
LOG.debug(
"[Secret] Text range of detection has been shifted. " +
"Annotation is not relevant to this state of the file content anymore"
)
return false
}

return true
}

private fun validateScaTextRange(textRange: TextRange, psiFile: PsiFile, expectedPackageName: String): Boolean {
// text range is dynamic and calculated from the line number,
// so we can't use the same validation as for secrets
// instead, we check if the package name is still in the text range
val detectedSubstr = psiFile.text.substring(textRange.startOffset, textRange.endOffset)
if (!detectedSubstr.contains(expectedPackageName)) {
LOG.debug(
"[SCA] Text range of detection has been shifted. " +
"Annotation is not relevant to this state of the file content anymore"
)
return false
}

return true
}

private fun validateTextRange(textRange: TextRange, psiFile: PsiFile): Boolean {
if (textRange.endOffset > psiFile.text.length || textRange.startOffset < 0) {
// check if text range fits in file

// case: row with detections has been deleted, but detection is still in the local results DB
LOG.debug("Text range of detection is out of file bounds")
return false
}

return true
applyAnnotationsForIac(psiFile, holder)
}

private fun applyAnnotationsForSecrets(psiFile: PsiFile, holder: AnnotationHolder) {
val scanResults = getScanResults(psiFile)
val latestScanResult = scanResults.getSecretResults()
if (latestScanResult !is CliResult.Success) {
return
}

val relevantDetections = latestScanResult.result.detections.filter { detection ->
detection.detectionDetails.getFilepath() == psiFile.virtualFile.path
}

relevantDetections.forEach { detection ->
val severity = convertSeverity(detection.severity)

val detectionDetails = detection.detectionDetails
val textRange = TextRange(
detectionDetails.startPosition,
detectionDetails.startPosition + detectionDetails.length
)

if (!validateTextRange(textRange, psiFile) || !validateSecretTextRange(textRange, psiFile)) {
return@forEach
}

val detectedValue = psiFile.text.substring(textRange.startOffset, textRange.endOffset)
detectionDetails.detectedValue = detectedValue

val message = detection.getFormattedMessage()
val title = CycodeBundle.message("annotationTitle", detection.getFormattedTitle())

var companyGuidelineMessage = ""
if (detectionDetails.customRemediationGuidelines != null) {
companyGuidelineMessage = CycodeBundle.message(
"secretsAnnotationTooltipCompanyGuideline",
detectionDetails.customRemediationGuidelines
)
}

val tooltip = CycodeBundle.message(
"secretsAnnotationTooltip",
detection.severity,
detection.type,
message,
detection.detectionRuleId,
detectionDetails.fileName,
detectionDetails.sha512,
companyGuidelineMessage
)
holder.newAnnotation(severity, title)
.range(textRange)
.tooltip(tooltip)
.withFix(
CycodeIgnoreIntentionQuickFix(
CliScanType.Secret,
CycodeIgnoreType.PATH,
detection.detectionDetails.getFilepath()
)
)
.withFix(
CycodeIgnoreIntentionQuickFix(
CliScanType.Secret,
CycodeIgnoreType.RULE,
detection.detectionRuleId
)
)
.withFix(CycodeIgnoreIntentionQuickFix(CliScanType.Secret, CycodeIgnoreType.VALUE, detectedValue))
.create()

}
SecretApplier(getScanResults(psiFile)).apply(psiFile, holder)
}

private fun applyAnnotationsForSca(psiFile: PsiFile, holder: AnnotationHolder) {
val scanResults = getScanResults(psiFile)
val latestScanResult = scanResults.getScaResults()
if (latestScanResult !is CliResult.Success) {
return
}

val relevantDetections = latestScanResult.result.detections.filter { detection ->
detection.detectionDetails.getFilepath() == psiFile.virtualFile.path
}

relevantDetections.forEach { detection ->
val severity = convertSeverity(detection.severity)

// SCA doesn't provide start and end positions, so we have to calculate them from the line number
val line = detection.detectionDetails.lineInFile - 1
val startOffset = psiFile.text.lines().take(line).sumOf { it.length + 1 }
val endOffset = startOffset + psiFile.text.lines()[line].length

val detectionDetails = detection.detectionDetails
val textRange = TextRange(startOffset, endOffset)

if (!validateTextRange(textRange, psiFile) || !validateScaTextRange(
textRange,
psiFile,
detectionDetails.packageName
)
) {
return@forEach
}

val title = CycodeBundle.message("annotationTitle", detection.getFormattedTitle())

var firstPatchedVersionMessage = ""
if (detectionDetails.alert?.firstPatchedVersion != null) {
firstPatchedVersionMessage = CycodeBundle.message(
"scaAnnotationTooltipFirstPatchedVersion",
detectionDetails.alert.firstPatchedVersion
)
}

var lockFileNote = ""
if (isSupportedLockFile(psiFile.virtualFile.name)) {
val packageFileName = getPackageFileForLockFile(psiFile.virtualFile.name)
lockFileNote = CycodeBundle.message("scaAnnotationTooltipLockFileNote", packageFileName)
}
ScaApplier(getScanResults(psiFile)).apply(psiFile, holder)
}

val tooltip = CycodeBundle.message(
"scaAnnotationTooltip",
detection.severity,
firstPatchedVersionMessage,
detection.message,
detection.detectionRuleId,
lockFileNote,
)
holder.newAnnotation(severity, title)
.range(textRange)
.tooltip(tooltip)
.withFix(
CycodeIgnoreIntentionQuickFix(
CliScanType.Sca,
CycodeIgnoreType.PATH,
detection.detectionDetails.getFilepath()
)
)
.withFix(
CycodeIgnoreIntentionQuickFix(
CliScanType.Sca,
CycodeIgnoreType.RULE,
detection.detectionRuleId
)
)
.create()
}
private fun applyAnnotationsForIac(psiFile: PsiFile, holder: AnnotationHolder) {
IacApplier(getScanResults(psiFile)).apply(psiFile, holder)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.cycode.plugin.annotators.annotationAppliers

import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.psi.PsiFile

abstract class AnnotationApplierBase {
abstract fun apply(psiFile: PsiFile, holder: AnnotationHolder)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.cycode.plugin.annotators.annotationAppliers

import com.cycode.plugin.CycodeBundle
import com.cycode.plugin.annotators.convertSeverity
import com.cycode.plugin.annotators.validateTextRange
import com.cycode.plugin.cli.CliResult
import com.cycode.plugin.cli.CliScanType
import com.cycode.plugin.intentions.CycodeIgnoreIntentionQuickFix
import com.cycode.plugin.intentions.CycodeIgnoreType
import com.cycode.plugin.services.ScanResultsService
import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile

class IacApplier(private val scanResults: ScanResultsService) : AnnotationApplierBase() {
private fun validateIacTextRange(textRange: TextRange, psiFile: PsiFile): Boolean {
// FIXME(MarshalX): for now, I dont see any way to validate the text range for IaC
// small explanation:
// - IaC doesn't provide end positions, so we have to calculate them from the line number (get the last character in the line)
// - we can't use the same validation as for SCA because value in the range is unknown (for SCA we expect package name)
return true
}

override fun apply(psiFile: PsiFile, holder: AnnotationHolder) {
val latestScanResult = scanResults.getIacResults()
if (latestScanResult !is CliResult.Success) {
return
}

val relevantDetections = latestScanResult.result.detections.filter { detection ->
detection.detectionDetails.getFilepath() == psiFile.virtualFile.path
}

relevantDetections.forEach { detection ->
val severity = convertSeverity(detection.severity)

// IaC doesn't provide start and end positions, so we have to calculate them from the line number
val line = detection.detectionDetails.lineInFile - 1
val startOffset = psiFile.text.lines().take(line).sumOf { it.length + 1 }
val endOffset = startOffset + psiFile.text.lines()[line].length

val detectionDetails = detection.detectionDetails
val textRange = TextRange(startOffset, endOffset)

if (!validateTextRange(textRange, psiFile) || !validateIacTextRange(textRange, psiFile)) {
return@forEach
}

val message = detection.getFormattedMessage()
val title = CycodeBundle.message("annotationTitle", detection.getFormattedTitle())

var companyGuidelineMessage = ""
if (detectionDetails.customRemediationGuidelines != null) {
companyGuidelineMessage = CycodeBundle.message(
"iacAnnotationTooltipCompanyGuideline",
detectionDetails.customRemediationGuidelines
)
}

val tooltip = CycodeBundle.message(
"iacAnnotationTooltip",
detection.severity,
message,
detectionDetails.infraProvider,
detection.detectionRuleId,
detectionDetails.fileName,
companyGuidelineMessage
)
holder.newAnnotation(severity, title)
.range(textRange)
.tooltip(tooltip)
.withFix(
CycodeIgnoreIntentionQuickFix(
CliScanType.Iac,
CycodeIgnoreType.PATH,
detection.detectionDetails.getFilepath()
)
)
.withFix(
CycodeIgnoreIntentionQuickFix(
CliScanType.Iac,
CycodeIgnoreType.RULE,
detection.detectionRuleId
)
)
.create()
}
}
}
Loading