Le blog technique

Toutes les astuces #tech des collaborateurs de PI Services.

#openblogPI

Retrouvez les articles à la une

Changer le mot de passe de n’importe quel compte local sans être administrateur (à condition d’en connaitre le mot de passe actuel)

Si vous avez déjà essayé de modifier le mot de passe d’un compte local Windows via la ligne de commande (cmd ou PowerShell) sans être administrateur de la machine, vous vous êtes probablement heurté à un mur : si la boite de dialogue Ctrl+Alt+Suppr > Modifier un mot de passe permet bien de modifier le mot de passe de n’importe quel compte, les commandes disponibles nativement (net user ou Set-LocalUser) ne permettent elles que de réinitialiser celui d’un autre utilisateur que soi-même; ce qui nécessite évidemment des permissions élevées (Administrateur) sur la machine.

Les cas d’usage ne sont pas courants, mais on peut par exemple rencontrer le cas où un utilisateur simple d’un poste de travail dispose d’un second compte à privilèges sur sa machine qu’il utilise pour effectuer certaines tâches avec des permissions plus importantes via une élévation ponctuelle (RunAs). Les bonnes pratiques dictent que ce compte dispose d’un mot de passe à complexité élevée et à durée de vie faible, ce qui rend son renouvellement peu pratique s’il n’est pas possible d’utiliser de copier/coller, soit exactement la situation rencontrée dans la boite de dialogue Modifier un mot de passe.

Heureusement il est bel et bien possible de modifier ce mot de passe via Powershell à l’aide de la méthode native ChangePassword() de la classe System.DirectoryServices.AccountManagement.UserPrincipal, dont je vous présente ci-après une implémentation sous forme d’une fonction Set-LocalUserPassword facile à utiliser.

Exemple 1 : Changer le mot de passe pour l’utilisateur local « Bob », sans préciser son ancien ni son nouveau mot de passe dans la commande. La fonction se chargera de vous les demander interactivement.

Set-LocalUserPassword -Identity "Bob"

Exemple 2 : Changer votre propre mot de passe, sans préciser son ancien ni son nouveau mot de passe dans la commande. Dans ce cas pas la peine d’indiquer non-plus votre nom d’utilisateur, la fonction se chargera de tout vous demander interactivement.

Set-LocalUserPassword

Exemple 3 : L’ancien et le nouveau mot de passe peuvent aussi être fournis sous forme de SecureString dans la commande à l’aide des paramètres -OldPassword et -NewPassword.

$OldPassword = "P@ssword1" | ConvertTo-SecureString -AsPlainText -Force
$NewPassword = "P@ssword2" | ConvertTo-SecureString -AsPlainText -Force
Set-LocalUserPassword -Identity "Jane" -OldPassword $OldPassword -NewPassword $NewPassword

Le script complet est dispo sous form de gist Github : Changes the password of any local user account using its current password for verification without requiring administrator permissions

Ou directement ci-dessous :

Function Set-LocalUserPassword
{
    <#
.SYNOPSIS
    Changes the password of a local user account using the current password for verification.

.DESCRIPTION
    This function utilizes the System.DirectoryServices.AccountManagement .NET namespace to change a local user's password.
    
    Unlike the standard 'Set-LocalUser' cmdlet or 'net user' command, this function utilizes the 'ChangePassword' method. 
    This allows standard (non-administrator) users to change the password of any local account, provided they know the current password in the same way as with Set-ADAccountPassword for Active Directory Accounts.

.PARAMETER Identity
    The SAMAccountName of the local user to modify. 
    If omitted, the function will prompt for the username. 
    If this prompt is left empty, it defaults to the currently logged-in user ($env:USERNAME).

.PARAMETER OldPassword
    The current valid password for the account, passed as a SecureString.
    If omitted, the function will prompt the user to enter it securely via the host UI.

.PARAMETER NewPassword
    The desired new password for the account, passed as a SecureString.
    If omitted, the function will prompt the user to enter it securely via the host UI.

.EXAMPLE
    Set-LocalUserPassword -Identity "Bob"

    Description
    -----------
    Attempts to change the password for local user "Bob". The function will interactively prompt for the Old and New passwords.

.EXAMPLE
    Set-LocalUserPassword

    Description
    -----------
    Prompts for the target username. If the user presses Enter without typing a name, it targets the current user. 
    Then prompts for the Old and New passwords.

.INPUTS
    System.String

.OUTPUTS
    None

.NOTES
    Requirements: .NET Framework 3.5 or later (System.DirectoryServices.AccountManagement)
#>
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false, Position = 0)]
        [String]$Identity,

        [Parameter(Mandatory = $false)]
        [securestring]$OldPassword,

        [Parameter(Mandatory = $false)]
        [securestring]$NewPassword
    )

    Add-Type -AssemblyName System.DirectoryServices.AccountManagement

    try
    {
        if ([string]::IsNullOrWhiteSpace($Identity))
        {
            $Identity = Read-Host -Prompt "Enter local username (Leave empty for current user)"
            if ([string]::IsNullOrWhiteSpace($Identity))
            { 
                $Identity = $env:USERNAME 
            }
        }

        Write-Verbose "Connecting to Local Machine Context..."
        $PrincipalContext = [System.DirectoryServices.AccountManagement.PrincipalContext]::new("Machine")

        Write-Verbose "Looking up user: $Identity"
        $User = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($PrincipalContext, $Identity)

        if ($null -eq $User)
        {
            throw "User '$Identity' could not be found on this computer."
        }

        if (-not $OldPassword)
        {
            $OldPassword = Read-Host -Prompt "Enter OLD password for $Identity" -AsSecureString
        }
        if (-not $NewPassword)
        {
            $NewPassword = Read-Host -Prompt "Enter NEW password for $Identity" -AsSecureString
        }

        # Convert SecureString to PlainText as this is what ChangePassword() method requires
        $OldPassPlain = [System.Net.NetworkCredential]::new("", $OldPassword).Password
        $NewPassPlain = [System.Net.NetworkCredential]::new("", $NewPassword).Password

        $User.ChangePassword($OldPassPlain, $NewPassPlain)
        
        Write-Host "Password successfully changed for user: $Identity" -ForegroundColor Green

    }
    catch
    {
        Write-Error $_.Exception.Message
        throw
    }
    finally
    {
        # Clean up unmanaged COM resources and plain text secret variables
        if ($null -ne $User) { $User.Dispose() }
        if ($null -ne $PrincipalContext) { $PrincipalContext.Dispose() }
        
        $OldPassPlain = $null
        $NewPassPlain = $null
    }
}

Python – Rundeck – Script pour Lister les jobs

Présentation du Script query_rundeck_pwd_auth.py

Ce script Python utilise la bibliothèque requests pour interagir avec l’API REST de Rundeck en suivant l’approche suivante :

  1. Authentification par Mot de Passe : Le script gère l’authentification en envoyant les identifiants (j_username et j_password) au point de terminaison /j_security_check de Rundeck. Cela établit une session, et le mot de passe est saisi de manière sécurisée (non affiché) grâce au module getpass.
  2. Collecte des Données :
    • Il commence par lister tous les projets disponibles.
    • Pour chaque projet, il liste les jobs.
    • Pour chaque job, une requête API est faite pour récupérer la dernière exécution en utilisant le paramètre max: 1. Le script parse le statut, les dates de début et de fin de cette dernière exécution.
  3. Sortie Standard et Exportation CSV :
    • Les données sont affichées dans la console pour une vérification immédiate.
    • Un fichier rundeck_jobs.csv est généré, fournissant un enregistrement structuré et facilement utilisable pour l’analyse ou l’audit. Le format de sortie est en semicolon-separated values (CSV) : EXPORT_DATE;PROJET;JOB_ID;JOB_NAME;DERNIERE_EXECUTION.

Utilisation

L’exécution du script se fait en fournissant l’URL du site Rundeck, l’URL de l’API et le nom d’utilisateur comme arguments :

python query_rundeck_pwd_auth.py site_url api_url user
python query_rundeck_pwd_auth.py ‘http://myrundeck.mydomain:4440’ ‘http://myrundeck.mydomain:4440/api/14’ myaccount

import requests
import json
from contextlib import redirect_stdout
import sys
import getpass
import datetime



if len(sys.argv) < 4:
    print('Usage: query_rundeck_pwd_auth.py <site_url> <api url> <user>')
    sys.exit()


objdate = datetime.datetime
# site url
rundeck_site_url = (sys.argv[1])
# api url
rundeck_api_url = (sys.argv[2])
# user
user = (sys.argv[3])
# password demandé de façon sécurisée (non affiché)
password = getpass.getpass(prompt='Mot de passe: ')


# Open session with user/password authentication
url = f'{rundeck_site_url}/j_security_check'
session = requests.session()
response = session.post(url, data={"j_username": user, "j_password": password})

# debug minimal : afficher erreur si auth KO
if response.status_code != 200:
    print(f"Auth error: {response.status_code}")
    try:
        print(response.text)
    except Exception:
        pass


# Fonction pour lister les projets
def lister_projects():
    url = f'{rundeck_api_url}/projects'
    headers = {
        'Accept': 'application/json'
            }
    response = session.get(url,headers = headers)

    allprojects=[]
    myproject={
          'project': None
          }

    if response.status_code == 200:
        projects = response.json()
        for project in projects:
            project_name=project['name']
            myproject={
                'project_name': project_name,
                 }
            allprojects.append(myproject)
        return allprojects    
    else:
            print(f"Erreur: {response.status_code}")
            try:
                print(response.text)
            except Exception:
                pass



# Fonction pour lister les jobs d'un projet
def lister_jobs(projet):
    url = f'{rundeck_api_url}/project/{projet}/jobs'
    headers = {
        'Accept': 'application/json'
        
    }
    response = session.get(url,headers = headers)

    alljobs=[]
    myjob={
          'export_date': None,
          'projet': None,
          'job_id': None,
          'job_name': None,
          'last_result': None
          }

    if response.status_code == 200:
        jobs = response.json()
        for job in jobs:
            job_id=job['id']
            job_name=job['name']
            last_result=dernier_etat_execution(job['id'])
            myjob={
                'export_date': objdate.now().strftime("%d/%m/%Y"),
                'projet': projet,
                'job_id': job_id,
                'job_name': job_name,
                'last_result': last_result
            }
            alljobs.append(myjob)
        return alljobs    
    else:
            print(f"Erreur: {response.status_code}")
            try:
                print(response.text)
            except Exception:
                pass


# Fonction pour obtenir le dernier état d'exécution d'un job
def dernier_etat_execution(job_id):
    url = f'{rundeck_api_url}/job/{job_id}/executions'
    headers = {
        'Accept': 'application/json'
        
    }
    params = {
        'max': 1,  # Limite à une exécution pour obtenir la plus récente
        'offset': 0
    }
    response = session.get(url, headers=headers, params=params)
    
    allexec=[]
    myexec={
          'id': None,
          'statut': None,
          'start': None,
          'end': None
          }



    if response.status_code == 200:
        executions = response.json()
        if executions['executions']:
            derniere_execution = executions['executions'][0]
            exec_id=derniere_execution['id']
            exec_statut=derniere_execution['status']
            exec_start=objdate.fromisoformat(derniere_execution['date-started']['date'].replace('Z','')).strftime("%d/%m/%Y, %H:%M:%S")
            # Si 'date-started' n'est pas trouvé dans la derniere execution on positionne exec_end='null'
            if 'date-ended' in derniere_execution: 
               exec_end=objdate.fromisoformat(derniere_execution['date-ended']['date'].replace('Z','')).strftime("%d/%m/%Y, %H:%M:%S")
            else:
               exec_end='null'
            myexec={
                'id': exec_id,
                'statut': exec_statut,
                'start': exec_start,
                'end': exec_end
            }
            allexec.append(myexec)
            return allexec
        else:
            return("Aucune exécution trouvée pour ce job.")
    else:
        return(f"Erreur: {response.status_code}")





# All projects
allproj=lister_projects()   

# Transforme la liste allproj en dictionnaire
dicallproj=dict(enumerate(allproj))



# Pour chacun des projets, recuperation de la liste des jobs et de l'état de la derniere execution
dicallprojjobs={}
for i in sorted(dicallproj.keys()):
    dicallprojjobs[i]=lister_jobs(dicallproj[i]['project_name'])



# Affichage du dictionnaire imbriqué
print("ALL PROJETS - ALL LAST JOBS:")
print("EXPORT_DATE;PROJET;JOB_ID;JOB_NAME;DERNIERE_EXECUTION")
for i in sorted(dicallprojjobs.items()):
    for x in i[1]:
        print("{export_date};{projet};{job_id};{job_name};{last_result}".format(export_date=x['export_date'],projet=x['projet'],job_id=x['job_id'], job_name=x['job_name'], last_result=x['last_result']))
    


# Export vers rundeck_jobs.csv du dictionnaire dicallprojjobs 
with open('rundeck_jobs.csv', 'w') as f:
    with redirect_stdout(f):
    
      print("EXPORT_DATE;PROJET;JOB_ID;JOB_NAME;DERNIERE_EXECUTION")
      for i in sorted(dicallprojjobs.items()):
        for x in i[1]:
          print("{export_date};{projet};{job_id};{job_name};{last_result}".format(export_date=x['export_date'],projet=x['projet'],job_id=x['job_id'], job_name=x['job_name'], last_result=x['last_result']))
 

print("\nresultat exporté dans rundeck_jobs.csv")

[Powershell] – Les dates au format Iso 8601

J’ai récemment eu besoin dans un script Powershell de construire une requête HTTP pour Microsoft API Graph, cela m’a permis de d’utiliser la norme Iso 8601 pour réaliser mes filtres.

Le format Iso 8601

La norme Iso 8601 spécifie la représentation numérique de la date et de l’heure, respectivement basées sur le calendrier grégorien et le système horaire sur 24 heures.

Voici la mise en forme de cette norme.

AAAA-MM-JJTHH:MM:SS,ss-/+FF:ff

AAAA: Représente l’année sur 4 chiffres
MM: représente le mois sur 2 chiffres (de 01 à 12)
JJ: représente le jour sur 2 chiffres (de 01 à 31)
T: Indique le temps (Time)
HH: représente l’heure sur 2 chiffres (de 00 à 24)
MM: représente les minutes sur 2 chiffres (de 00 à 59)
SS: représente les secondes sur 2 chiffres (de 00 à 59)
ss: représente la fraction de secondes sur 1 ou 2 chiffres
+/-: représente le fuseau horaire, où ‘+’ indique une avance sur UTC et ‘-‘ un retard sur UTC
FF: représente le nombre d’heure d’avance ou de retard sur UTC
ff: représente le nombre de minute d’avance ou de retard sur UTC
Z: indique qu’il s’agit de l’heure au format UTC (pour méridien Zéro)

Comment l’utiliser avec Powershell

Donc pour formater la date et l’heure à la norme Iso 8601, il suffit d’utiliser la commande suivante :

(Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ")

Comme vous pourrez le constater, dans mon cas j’utilise la lettre « Z » pour obtenir directement le format UTC.