diff --git a/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/utils/Chmod.java b/iac-common/src/main/java/org/sonar/iac/common/checks/Chmod.java similarity index 60% rename from iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/utils/Chmod.java rename to iac-common/src/main/java/org/sonar/iac/common/checks/Chmod.java index eb8187aaf4..58d137d6e0 100644 --- a/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/utils/Chmod.java +++ b/iac-common/src/main/java/org/sonar/iac/common/checks/Chmod.java @@ -17,38 +17,30 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.iac.docker.checks.utils; +package org.sonar.iac.common.checks; -import org.sonar.iac.docker.symbols.ArgumentResolution; -import org.sonar.iac.docker.tree.api.Argument; - -import javax.annotation.Nullable; -import java.util.ArrayList; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.IntStream; /** - * Represent chmod call instruction in RUN Arguments, with parsed permissions ready to be checked + * Represent chmod call instruction, with parsed permissions ready to be checked */ public class Chmod { - private static final Pattern CHMOD_OPTIONS_PATTERN = Pattern.compile("-[a-zA-Z]|--[a-zA-Z-]++"); private static final String NUMERIC = "(?[0-7]{1,4})"; private static final String ALPHANUMERIC = "[ugoa]*+[=+-][rwxXstugo]++"; private static final String ALPHANUMERICS = "(?(?:" + ALPHANUMERIC + ",?+)++)"; private static final Pattern PERMISSIONS_PATTERN = Pattern.compile(NUMERIC + "|" + ALPHANUMERICS); - public final Argument chmodArg; - public final Argument permissionsArg; - public final Permission permissions; + private final Permission permissions; + + public static Chmod fromString(String permissions) { + return new Chmod(parsePermissions(permissions)); + } - public Chmod(@Nullable Argument chmodArg, @Nullable Argument permissionsArg, String permissions) { - this.chmodArg = chmodArg; - this.permissionsArg = permissionsArg; - this.permissions = parsePermissions(permissions); + private Chmod(Permission permissions) { + this.permissions = permissions; } private static Permission parsePermissions(String permissions) { @@ -64,51 +56,25 @@ private static Permission parsePermissions(String permissions) { } } - public static List extractChmodsFromArguments(List arguments) { - List chmods = new ArrayList<>(); - List argumentsStrings = arguments.stream() - .map(arg -> ArgumentResolution.of(arg).value()) - .toList(); - - List chmodIndexes = findChmodIndexes(argumentsStrings); - for (Integer chmodIndex : chmodIndexes) { - Integer indexPermissions = skipOptions(chmodIndex, argumentsStrings); - if (indexPermissions != null) { - chmods.add(new Chmod(arguments.get(chmodIndex), arguments.get(indexPermissions), argumentsStrings.get(indexPermissions))); - } - } - - return chmods; - } - - private static List findChmodIndexes(List arguments) { - return IntStream.range(0, arguments.size()) - .filter(i -> "chmod".equals(arguments.get(i))) - .boxed() - .toList(); - } - - private static Integer skipOptions(int index, List arguments) { - do { - index++; - } while (index < arguments.size() && arguments.get(index) != null && CHMOD_OPTIONS_PATTERN.matcher(arguments.get(index)).matches()); - return index < arguments.size() ? index : null; - } - + /** + * Checks if it contains permission in alphanumeric format, examples: o+x, g+r, u+w, u+s, g+s, +t. + * @param right permission in alphanumeric format + * @return true if contains the permission, false otherwise + */ public boolean hasPermission(String right) { return permissions.rights.contains(right); } /** - * Class dedicated to store permissions in the chmod way : man chmod + * Class dedicated to store permissions in the chmod way: man chmod */ public static class Permission { private Permission() { } - // Store permissions at the following format : + + // Store permissions at the alphanumeric format: + // Example : "u+w" -> user has write permission - Set rights = new HashSet<>(); + private final Set rights = new HashSet<>(); static Permission empty() { return new Permission(); @@ -129,7 +95,7 @@ public static Permission fromAlphanumeric(String alphanumerics) { public static Permission fromNumeric(String numeric) { Permission chmodRight = new Permission(); numeric = ("0000" + numeric).substring(numeric.length()); - chmodRight.addSet(numeric.charAt(0)); + chmodRight.addSetIdOnExecutionOrDetectionFlagOrStickyBit(numeric.charAt(0)); chmodRight.addRight(numeric.charAt(1), 'u'); chmodRight.addRight(numeric.charAt(2), 'g'); chmodRight.addRight(numeric.charAt(3), 'o'); @@ -143,10 +109,13 @@ private void addRight(char digit, char target) { addIfFlag(target + "+x", value, 0b001); } - private void addSet(char digit) { + private void addSetIdOnExecutionOrDetectionFlagOrStickyBit(char digit) { int value = digit - '0'; + // set user ID on execution addIfFlag("u+s", value, 0b100); + // set group ID on execution addIfFlag("g+s", value, 0b010); + // restricted deletion flag or sticky bit addIfFlag("+t", value, 0b001); } @@ -167,9 +136,5 @@ private void addRights(String targets, String rights) { } } } - - public boolean hasRight(String right) { - return rights.contains(right); - } } } diff --git a/iac-common/src/test/java/org/sonar/iac/common/checks/ChmodTest.java b/iac-common/src/test/java/org/sonar/iac/common/checks/ChmodTest.java new file mode 100644 index 0000000000..ae7671a775 --- /dev/null +++ b/iac-common/src/test/java/org/sonar/iac/common/checks/ChmodTest.java @@ -0,0 +1,198 @@ +/* + * SonarQube IaC Plugin + * Copyright (C) 2021-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.iac.common.checks; + +import java.util.List; +import java.util.stream.Stream; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ChmodTest { + + @ParameterizedTest + @MethodSource + void shouldContainsPermissions(String expectedPermission, List permissions) { + SoftAssertions.assertSoftly(softly -> { + for (String permission : permissions) { + var chmod = Chmod.fromString(permission); + softly.assertThat(chmod.hasPermission(expectedPermission)) + .overridingErrorMessage("Expected '%s' permission but not found for mode '%s'", expectedPermission, permission) + .isTrue(); + } + }); + } + + static Stream shouldContainsPermissions() { + return Stream.of( + Arguments.arguments("o+x", List.of( + "1", "01", "001", "0001", "1111", "7771", + "3", "03", "003", "0003", "1113", "7773", + "5", "05", "005", "0005", "1115", "7775", + "7", "07", "007", "0007", "1117", "7777", + "o+x", "o=x", "+x", "o+r,o+x", "o+x,o+r", "u+w,o+x", "o+rx", "o+rwx", "o+xX")), + Arguments.arguments("o+w", List.of( + "2", "02", "002", "0002", "1112", "7772", + "3", "03", "003", "0003", "1113", "7773", + "6", "06", "006", "0006", "1116", "7776", + "7", "07", "007", "0007", "1117", "7777", + "o+w", "o=w", "+w", "o+r,o+w", "o+w,o+r", "u+r,o+w", "o+rw", "o+rwx", "o+wX")), + Arguments.arguments("o+r", List.of( + "4", "04", "004", "0004", "4444", "7774", + "5", "05", "005", "0005", "1115", "7775", + "6", "06", "006", "0006", "1116", "7776", + "7", "07", "007", "0007", "1117", "7777", + "o+r", "o=r", "+r", "o+w,o+r", "o+r,o+w", "u+x,o+r", "o+rw", "o+rwx", "o+rX")), + Arguments.arguments("g+x", List.of( + "10", "010", "0010", "1111", "7717", + "30", "030", "0030", "1131", "7737", + "50", "050", "0050", "1151", "7757", + "70", "070", "0070", "1171", "7777", + "g+x", "g=x", "g+r,g+x", "g+x,g+r", "u+w,g+x", "g+rx", "g+rwx", "g+xX")), + Arguments.arguments("g+w", List.of( + "20", "020", "0020", "1121", "7727", + "30", "030", "0030", "1131", "7737", + "60", "060", "0060", "1161", "7767", + "70", "070", "0070", "1171", "7777", + "g+w", "g=w", "g+r,g+w", "g+w,g+r", "u+w,g+w", "g+rw", "g+rwx", "g+wX")), + Arguments.arguments("g+r", List.of( + "40", "040", "0040", "1141", "7747", + "50", "050", "0050", "1151", "7757", + "60", "060", "0060", "1161", "7767", + "70", "070", "0070", "1171", "7777", + "g+r", "g=r", "g+r,g+w", "g+w,g+r", "u+w,g+r", "g+rw", "g+rwx", "g+rX")), + Arguments.arguments("u+x", List.of( + "100", "0100", "1111", "7177", + "300", "0300", "1311", "7377", + "500", "0500", "1511", "7577", + "700", "0700", "1711", "7777", + "u+x", "u=x", "u+r,u+x", "u+x,u+r", "g+w,u+x", "u+rx", "u+rwx", "u+xX")), + Arguments.arguments("u+w", List.of( + "200", "0200", "1211", "7277", + "300", "0300", "1311", "7377", + "600", "0600", "1611", "7677", + "700", "0700", "1711", "7777", + "u+w", "u=w", "u+r,u+w", "u+w,u+r", "g+w,u+w", "u+rw", "u+rwx", "u+wX")), + Arguments.arguments("u+r", List.of( + "400", "0400", "1411", "7477", + "500", "0500", "1511", "7577", + "600", "0600", "1611", "7677", + "700", "0700", "1711", "7777", + "u+r", "u=r", "u+r,u+w", "u+w,u+r", "g+w,u+r", "u+rw", "u+rwx", "u+rX")), + // setting sticky bit using `+t` is not supported yet, and it is not used in checks yet + // "+t", "=t", "o+r,+t", "+t,o+r", "u+w,+t", "u+s,+t", "+t,g+s" + Arguments.arguments("+t", List.of( + "1000", "1111", "1777", + "3000", "3111", "3777", + "5000", "5111", "5777", + "7000", "7111", "7777")), + Arguments.arguments("g+s", List.of( + "2000", "2111", "2777", + "3000", "3111", "3777", + "6000", "6111", "6777", + "7000", "7111", "7777", + "g+s", "g=s", "g+r,g+s", "g+s,g+r", "u+w,g+s", "g+rs", "g+rwxs", "g+sX")), + Arguments.arguments("u+s", List.of( + "4000", "4111", "4777", + "5000", "5111", "5777", + "6000", "6111", "6777", + "7000", "7111", "7777", + "u+s", "u=s", "u+r,u+s", "u+s,u+r", "o+w,u+s", "u+rs", "u+rwxs", "u+sX"))); + } + + @ParameterizedTest + @MethodSource + void shouldNotContainsPermissions(String expectedPermission, List permissions) { + SoftAssertions.assertSoftly(softly -> { + for (String permission : permissions) { + var chmod = Chmod.fromString(permission); + softly.assertThat(chmod.hasPermission(expectedPermission)) + .overridingErrorMessage("Do NOT expected '%s' permission but not found for mode '%s'", expectedPermission, permission) + .isFalse(); + } + }); + } + + static Stream shouldNotContainsPermissions() { + return Stream.of( + Arguments.arguments("o+x", List.of( + "2", "02", "002", "0002", "2222", "7772", + "4", "04", "004", "0004", "1114", "7774", + "6", "06", "006", "0006", "1116", "7776", + "o-x", "o=r", "o=w", "+r", "o+r,o+w", "o+w,o+r", "u+w,o+r", "o+rw", "o+rX")), + Arguments.arguments("o+w", List.of( + "1", "01", "001", "0001", "1111", "7771", + "4", "04", "004", "0004", "1114", "7774", + "5", "05", "005", "0005", "1115", "7775", + "o-w", "o=r", "o=x", "+r", "o+r,o+x", "o+x,o+r", "u+x,o+r", "o+rx", "o+rX")), + Arguments.arguments("o+r", List.of( + "1", "01", "001", "0001", "1111", "7771", + "2", "02", "002", "0002", "1112", "7772", + "3", "03", "003", "0003", "1113", "7773", + "o-r", "o=x", "o=w", "+x", "o+w,o+x", "o+x,o+w", "u+r,o+w", "o+wx", "o+wX")), + Arguments.arguments("g+x", List.of( + "20", "020", "0020", "1121", "7727", + "40", "040", "0040", "1141", "7747", + "60", "060", "0060", "1161", "7767", + "g-x", "g=r", "g=w", "g+r,g+w", "g+w,g+r", "u+w,g+r", "g+rw", "g+rX")), + Arguments.arguments("g+w", List.of( + "10", "010", "0010", "1111", "7717", + "40", "040", "0040", "1141", "7747", + "50", "050", "0050", "1151", "7757", + "g-w", "g=r", "g=x", "g+r,g+x", "g+x,g+r", "u+w,g+r", "g+rx", "g+rX")), + Arguments.arguments("g+r", List.of( + "10", "010", "0010", "1111", "7717", + "20", "020", "0020", "1121", "7727", + "30", "030", "0030", "1131", "7737", + "g-r", "g=w", "g=x", "g+w,g+x", "g+x,g+w", "u+r,g+w", "g+wx", "g+wX")), + Arguments.arguments("u+x", List.of( + "200", "0200", "1211", "7277", + "400", "0400", "1411", "7477", + "600", "0600", "1611", "7677", + "u-x", "u=r", "u=w", "u+r,u+w", "u+w,u+r", "g+x,u+r", "u+rw", "u+rX")), + Arguments.arguments("u+w", List.of( + "100", "0100", "1111", "7177", + "400", "0400", "1411", "7477", + "500", "0500", "1511", "7577", + "u-w", "u=r", "u=x", "u+r,u+x", "u+x,u+r", "g+w,u+r", "u+rx", "u+xX")), + Arguments.arguments("u+r", List.of( + "100", "0100", "1111", "7177", + "200", "0200", "1211", "7277", + "300", "0300", "1311", "7377", + "u-r", "u=w", "u=x", "u+w,u+x", "u+x,u+w", "g+w,u+w", "u+wx", "u+xX")), + Arguments.arguments("+t", List.of( + // setting sticky bit using `+t` is not supported yet, and it is not used in checks yet + "+t", "=t", "o+r,+t", "+t,o+r", "u+w,+t", "u+s,+t", "+t,g+s", + "2000", "2111", "2777", + "4000", "4111", "4777", + "6000", "6111", "6777")), + Arguments.arguments("g+s", List.of( + "1000", "1111", "1777", + "4000", "4111", "4777", + "5000", "5111", "5777", + "g-s", "g=w", "g=x", "g=r", "g+r,g+w", "g+w,g+r", "u+s,g+w", "g+rw", "g+rwx", "g+rX")), + Arguments.arguments("u+s", List.of( + "1000", "1111", "1777", + "2000", "2111", "2777", + "3000", "3111", "3777", + "u-s", "u=w", "u=r", "u=x", "u+r,u+w", "u+w,u+r", "u+w,g+s", "u+rw", "u+rwx", "u+rX"))); + } +} diff --git a/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/ExecutableNotOwnedByRootCheck.java b/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/ExecutableNotOwnedByRootCheck.java index 67fa9808fd..70f16bbd0f 100644 --- a/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/ExecutableNotOwnedByRootCheck.java +++ b/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/ExecutableNotOwnedByRootCheck.java @@ -29,7 +29,7 @@ import org.sonar.iac.common.api.checks.InitContext; import org.sonar.iac.common.api.checks.SecondaryLocation; import org.sonar.iac.common.api.tree.HasTextRange; -import org.sonar.iac.docker.checks.utils.Chmod; +import org.sonar.iac.docker.checks.utils.ArgumentChmod; import org.sonar.iac.docker.symbols.ArgumentResolution; import org.sonar.iac.docker.tree.api.Argument; import org.sonar.iac.docker.tree.api.Flag; @@ -50,16 +50,16 @@ public void initialize(InitContext init) { } private static void checkTransferInstruction(CheckContext ctx, TransferInstruction transferInstruction) { - Flag sensitiveChownFlag = getSensitiveChownFlag(transferInstruction); + var sensitiveChownFlag = getSensitiveChownFlag(transferInstruction); if (sensitiveChownFlag != null) { - List sensitiveFiles = transferInstruction.srcs(); + var sensitiveFiles = transferInstruction.srcs(); if (isNonRootUser(sensitiveChownFlag)) { reportIssue(ctx, sensitiveChownFlag, sensitiveFiles); } - Chmod chmod = getChmod(transferInstruction); + var chmod = getChmod(transferInstruction); if (chmod == null) { if (!sensitiveFiles.isEmpty()) { reportIssue(ctx, sensitiveChownFlag, sensitiveFiles); @@ -70,7 +70,7 @@ private static void checkTransferInstruction(CheckContext ctx, TransferInstructi } } - private static boolean isSensitiveChmod(Flag sensitiveChownFlag, List sensitiveFiles, Chmod chmod) { + private static boolean isSensitiveChmod(Flag sensitiveChownFlag, List sensitiveFiles, ArgumentChmod chmod) { return !isRootUserAndGroupHasNoWritePermission(sensitiveChownFlag, chmod) && isSensitiveWriteChmod(chmod) && (isSensitiveExecuteChmod(chmod) || !sensitiveFiles.isEmpty()); @@ -100,40 +100,40 @@ private static Flag getSensitiveChownFlag(TransferInstruction transferInstructio } private static boolean isSensitiveUser(Flag chownFlag) { - ArgumentResolution resolvedArgArgument = ArgumentResolution.of(chownFlag.value()); + var resolvedArgArgument = ArgumentResolution.of(chownFlag.value()); return resolvedArgArgument.isResolved() && isNonRootChown(resolvedArgArgument.value()); } @CheckForNull - private static Chmod getChmod(TransferInstruction transferInstruction) { + private static ArgumentChmod getChmod(TransferInstruction transferInstruction) { return transferInstruction.options().stream() .filter(f -> f.name().equals("chmod")) .map(f -> ArgumentResolution.of(f.value())) .filter(ArgumentResolution::isResolved) - .map(argResolved -> new Chmod(null, null, argResolved.value())) + .map(argResolved -> new ArgumentChmod(null, null, argResolved.value())) .findFirst() .orElse(null); } - private static boolean isSensitiveWriteChmod(Chmod chmod) { + private static boolean isSensitiveWriteChmod(ArgumentChmod chmod) { return chmod.hasPermission("u+w") || chmod.hasPermission("g+w"); } - private static boolean isSensitiveExecuteChmod(Chmod chmod) { + private static boolean isSensitiveExecuteChmod(ArgumentChmod chmod) { return chmod.hasPermission("u+x") || chmod.hasPermission("g+x") || chmod.hasPermission("o+x"); } // true if user value is any of ['root', '0', ''], group value is not from this list, and group has write permissions // for example --chown=root:bar --chmod=664 - private static boolean isRootUserAndGroupHasNoWritePermission(Flag chown, Chmod chmod) { - ArgumentResolution resolvedChown = ArgumentResolution.of(chown.value()); - boolean isRootUser = !isNonRootAtId(resolvedChown.value(), 0); + private static boolean isRootUserAndGroupHasNoWritePermission(Flag chown, ArgumentChmod chmod) { + var resolvedChown = ArgumentResolution.of(chown.value()); + var isRootUser = !isNonRootAtId(resolvedChown.value(), 0); return !chmod.hasPermission("g+w") && isRootUser && isNonRootAtId(resolvedChown.value(), 1); } // true if the user value is different from ['root', '0', ''], for example 'foo:root' private static boolean isNonRootUser(Flag sensitiveChownFlag) { - ArgumentResolution resolvedChown = ArgumentResolution.of(sensitiveChownFlag.value()); + var resolvedChown = ArgumentResolution.of(sensitiveChownFlag.value()); return isNonRootAtId(resolvedChown.value(), 0); } @@ -144,7 +144,7 @@ private static boolean isNonRootChown(String chownValue) { // true if the value at the specified id (0 for user, 1 for group) is not from ['root', '0', ''] static boolean isNonRootAtId(String chownValue, int indexToCheck) { - String[] split = chownValue.split(":"); + var split = chownValue.split(":"); if (split.length > indexToCheck) { return !COMPLIANT_CHOWN_VALUES.contains(split[indexToCheck]); } diff --git a/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/PosixPermissionCheck.java b/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/PosixPermissionCheck.java index 7c3bde0a12..b25411d62d 100644 --- a/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/PosixPermissionCheck.java +++ b/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/PosixPermissionCheck.java @@ -27,7 +27,8 @@ import org.sonar.iac.common.api.checks.IacCheck; import org.sonar.iac.common.api.checks.InitContext; import org.sonar.iac.common.api.tree.impl.TextRanges; -import org.sonar.iac.docker.checks.utils.Chmod; +import org.sonar.iac.common.checks.Chmod; +import org.sonar.iac.docker.checks.utils.ArgumentChmod; import org.sonar.iac.docker.symbols.ArgumentResolution; import org.sonar.iac.docker.tree.api.Argument; import org.sonar.iac.docker.tree.api.RunInstruction; @@ -45,7 +46,7 @@ public void initialize(InitContext init) { } private static void checkRunChmodPermission(CheckContext ctx, RunInstruction runInstruction) { - for (Chmod chmod : Chmod.extractChmodsFromArguments(runInstruction.arguments())) { + for (ArgumentChmod chmod : ArgumentChmod.extractChmodsFromArguments(runInstruction.arguments())) { if (chmod.hasPermission("o+w") || chmod.hasPermission("g+s") || chmod.hasPermission("u+s")) { TextRange textRange = TextRanges.merge(List.of(chmod.chmodArg.textRange(), chmod.permissionsArg.textRange())); ctx.reportIssue(textRange, MESSAGE); @@ -68,6 +69,6 @@ private static boolean isPermissionSensitive(@Nullable Argument permission) { if (permissionString == null) { return false; } - return Chmod.Permission.fromNumeric(permissionString).hasRight("o+w"); + return Chmod.fromString(permissionString).hasPermission("o+w"); } } diff --git a/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/utils/ArgumentChmod.java b/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/utils/ArgumentChmod.java new file mode 100644 index 0000000000..539ea34807 --- /dev/null +++ b/iac-extensions/docker/src/main/java/org/sonar/iac/docker/checks/utils/ArgumentChmod.java @@ -0,0 +1,81 @@ +/* + * SonarQube IaC Plugin + * Copyright (C) 2021-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.iac.docker.checks.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.IntStream; +import javax.annotation.Nullable; +import org.sonar.iac.common.checks.Chmod; +import org.sonar.iac.docker.symbols.ArgumentResolution; +import org.sonar.iac.docker.tree.api.Argument; + +/** + * Represent chmod call instruction in RUN Arguments, with parsed permissions ready to be checked + */ +public class ArgumentChmod { + private static final Pattern FLAG_PATTERN = Pattern.compile("-[a-zA-Z]|--[a-zA-Z-]++"); + + public final Argument chmodArg; + public final Argument permissionsArg; + public final Chmod chmod; + + public ArgumentChmod(@Nullable Argument chmodArg, @Nullable Argument permissionsArg, String chmod) { + this.chmodArg = chmodArg; + this.permissionsArg = permissionsArg; + this.chmod = Chmod.fromString(chmod); + } + + public static List extractChmodsFromArguments(List arguments) { + List chmods = new ArrayList<>(); + List argumentsStrings = arguments.stream() + .map(arg -> ArgumentResolution.of(arg).value()) + .toList(); + + List chmodIndexes = findChmodIndexes(argumentsStrings); + for (Integer chmodIndex : chmodIndexes) { + Integer indexPermissions = skipOptions(chmodIndex, argumentsStrings); + if (indexPermissions != null) { + chmods.add(new ArgumentChmod(arguments.get(chmodIndex), arguments.get(indexPermissions), argumentsStrings.get(indexPermissions))); + } + } + + return chmods; + } + + private static List findChmodIndexes(List arguments) { + return IntStream.range(0, arguments.size()) + .filter(i -> "chmod".equals(arguments.get(i))) + .boxed() + .toList(); + } + + private static Integer skipOptions(int index, List arguments) { + do { + index++; + } while (index < arguments.size() && arguments.get(index) != null && FLAG_PATTERN.matcher(arguments.get(index)).matches()); + return index < arguments.size() ? index : null; + } + + public boolean hasPermission(String right) { + return chmod.hasPermission(right); + } +}