Skip to content

Commit

Permalink
SONARIAC-1727 Extract chmod permissions to iac-commons (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
mstachniuk authored and sonartech committed Oct 17, 2024
1 parent 2d9bd97 commit 65fbfba
Show file tree
Hide file tree
Showing 5 changed files with 320 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "(?<numeric>[0-7]{1,4})";
private static final String ALPHANUMERIC = "[ugoa]*+[=+-][rwxXstugo]++";
private static final String ALPHANUMERICS = "(?<alphanumeric>(?:" + 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) {
Expand All @@ -64,51 +56,25 @@ private static Permission parsePermissions(String permissions) {
}
}

public static List<Chmod> extractChmodsFromArguments(List<Argument> arguments) {
List<Chmod> chmods = new ArrayList<>();
List<String> argumentsStrings = arguments.stream()
.map(arg -> ArgumentResolution.of(arg).value())
.toList();

List<Integer> 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<Integer> findChmodIndexes(List<String> arguments) {
return IntStream.range(0, arguments.size())
.filter(i -> "chmod".equals(arguments.get(i)))
.boxed()
.toList();
}

private static Integer skipOptions(int index, List<String> 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 : <a href="https://linux.die.net/man/1/chmod">man chmod</a>
* Class dedicated to store permissions in the chmod way: <a href="https://linux.die.net/man/1/chmod">man chmod</a>
*/
public static class Permission {
private Permission() {
}

// Store permissions at the following format : <target>+<right>
// Store permissions at the alphanumeric format: <target>+<right>
// Example : "u+w" -> user has write permission
Set<String> rights = new HashSet<>();
private final Set<String> rights = new HashSet<>();

static Permission empty() {
return new Permission();
Expand All @@ -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');
Expand All @@ -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);
}

Expand All @@ -167,9 +136,5 @@ private void addRights(String targets, String rights) {
}
}
}

public boolean hasRight(String right) {
return rights.contains(right);
}
}
}
198 changes: 198 additions & 0 deletions iac-common/src/test/java/org/sonar/iac/common/checks/ChmodTest.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<Arguments> 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<String> 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<Arguments> 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")));
}
}
Loading

0 comments on commit 65fbfba

Please sign in to comment.