Looking for more efficient way to pass arguments to subprocess.Popen - python-3.x

I have received a task that requires a python script to run on devices in our environment. The script should check for a certain criteria and generate an empty file in a subdirectory of /var/opt.
The script is called by a 3rd party application which results in the file being generated with this selinux context type "unconfined_u:object_r:var_t:s0". The script should update the selinux context type to "system_u:object_r:var_t:s0"
I originally create the script to use the python selinux module, but during testing we found that a large number of devices do not have the selinux module installed.
I modified the script to use the Linux commands (semanage and restorecon) and although it works, it just does not look right. I am still learning so if any guidance on how this code could be more efficient, it would be greatly appreciated.
Current Code:
if os.path.isfile("/usr/sbin/selinuxenabled"):
userdir = "/var/opt/abcdir"
check_se = subprocess.run(["/usr/sbin/selinuxenabled"]).returncode
# Set command and arguments for semanage command
se_cmd = "/usr/sbin/semanage"
se_args1 = "fcontext"
se_args2 = "-a"
se_args3 = "-t"
se_args4 = "var_t"
se_args5 = "-s system_u"
# Set command and arguments for restorecon command
re_cmd = "/usr/sbin/restorecon"
re_args1 = "-FR"
if check_se == 0:
log.info("selinux enabled")
subprocess.Popen([se_cmd,
se_args1,
se_args2,
se_args3,
se_args4,
se_args5,
userdir],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)

Related

Python3 - Sanitizing user input for shell use

I am busy writing a Python3 script which requires user input, the input is used as parameters in commands passed to the shell.
The script is only intended to be used by trusted internal users - however I'd rather have some contingencies in place to ensure the valid execution of commands.
Example 1:
import subprocess
user_input = '/tmp/file.txt'
subprocess.Popen(['cat', user_input])
This will output the contents of '/tmp/file.txt'
Example 2:
import subprocess
user_input = '/tmp/file.txt && rm -rf /'
subprocess.Popen(['cat', user_input])
Results in (as expected):
cat: /tmp/file.txt && rm -rf /: No such file or directory
Is this an acceptable method of sanitizing input? Is there anything else, per best practice, I should be doing in addition to this?
The approach you have chosen,
import subprocess
user_input = 'string'
subprocess.Popen(['command', user_input])
is quite good as command is static and user_input is passed as one single argument to command. As long as you don't do something really stupid like
subprocess.Popen(['bash', '-c', user_input])
you should be on the safe side.
For commands that require multiple arguments, I'd recommend that you request multiple inputs from the user, e.g. do this
user_input1='file1.txt'
user_input2='file2.txt'
subprocess.Popen(['cp', user_input1, user_input2])
instead of this
user_input="file1.txt file2.txt"
subprocess.Popen(['cp'] + user_input.split())
If you want to increase security further, you could:
explicitly set shell=False (to ensure you never run shell commands; this is already the current default, but defaults may change over time):
subprocess.Popen(['command', user_input], shell=False)
use absolute paths for command (to prevent injection of malicious executables via PATH):
subprocess.Popen(['/usr/bin/command', user_input])
explicitly instruct commands that support it to stop parsing options, e.g.
subprocess.Popen(['rm', '--', user_input1, user_input2])
do as much as you can natively, e.g. cat /tmp/file.txt could be accomplished with a few lines of Python code instead (which would also increase portability if that should be a factor)

Why does python's Popen fail to pass environment variables on Mac OS X?

I am writing a program that needs to spawn a new terminal window and launch a server in this new terminal window (with environment variables passed to the child process).
I have been able to achieve this on windows 10 and linux without much trouble but on Mac OS X (Big Sur) the environment variables are not being passed to the child process. Here is an example code snippet capturing the behaviour I want to achieve:
#!/usr/bin/python3
import subprocess
import os
command = "bash -c 'export'"
env = os.environ.copy()
env["MYVAR"] = "VAL"
process = subprocess.Popen(['osascript', '-e', f"tell application \"Terminal\" to do script \"{command}\""], env=env)
Unfortunately, MYVAR is not present in the exported environment variables.
Any ideas if I am doing something wrong here?
Is this a bug in python's standard library ('subprocess' module)?
edit - thank you Ben Paterson (previously my example code had a bug) - I have updated the code example but I still have the same issue.
edit - I have narrowed this down further. subprocess.Popen is doing what it is supposed to do with environment variables when I do:
command = "bash -c 'export > c.txt'"
process = subprocess.Popen(shlex.split(command, posix=1), env=env)
But when I try to wrap the command with osascript -e ... (to spawn it in a new terminal window) the environment variable "MYVAR" does not appear in the c.txt file.
command = "bash -c 'export > c.txt'"
process = subprocess.Popen(['osascript', '-e', f"tell application \"Terminal\" to do script \"{command}\""], env=env)
dict.update returns None, so the OP code is equivalent to passing env=None to subprocess.Popen. Write instead:
env = os.environ.copy()
env["MYVAR"] = "VAL"
subprocess.Popen(..., env=env)

Run Python scour SVG optimizer from script in stead of the CLI

scour for Python does a great job to reduce the size of the SVG maps that I have generated using matplotlib.
However, the documentation only describes how to run it for a single file from the CLI, standard:
scour -i input.svg -o output.svg
What is the best way to run it (in batch mode) from a Python script / from my Jupyter notebook?
I succeeded to 'create' the working code below and optimized 500 SVGs to my satisfaction. But I just copied that code in bits from the testscour.py and I lack the understanding about it...
from scour.scour import makeWellFormed, parse_args, scourString, scourXmlFile, start, run
maps= os.listdir('C:\\Hugo\\Sites\\maps\\')
for m in maps[:]:
if afbeelding[-4:]=='.svg':
inputfile = open('C:\\Hugo\\Sites\\maps\\' + m, 'rb')
outputfile = open('C:\\Hugo\\Sites\\allecijfers\\static\\images\\maps\\' + m, 'wb')
start(options, inputfile, outputfile)
Related questions (trying to learn): What is the best way to decompose a module like scour in order to find the right callable functions? (I would not have found the function start in scour.py ) Or could I have called the CLI command from the Python script above in order to execute it in batch mode?
I like your question because it pointed me to testscour.py, thanks a lot.
That said, the code imports scour first,
then makes makes a list from your map folder;
then goes through the list one by one, if it is an svg, it uses the "start" method to call scour.
so the working piece is
start(options, inputfile, outputfile)
Note that it needs an opened file handler, usually you make that with "with".
Have fun
To use Scour programmatically first install it using PIP.
pip install scour
Then add this to your code (using whatever names you like).
from scour.scour import start as scour, parse_args as scour_args, getInOut as scour_io
def scour_svg(infilename,outfilename):
options = scour_args()
options.infilename = infilename
options.outfilename = outfilename
// Set options here or accept defaults.
(input, output) = scour_io(options)
scour(options, input, output)
if __name__ == '__main__':
infilename = 'C:\\Users\\...\\svg_in.svg'
outfilename = 'C:\\Users\\...\\svg_scoured.svg'
scour_svg(infilename,outfilename)
print('The END')
Use the scour_svg function to read in an SVG file, scour it, then write it out to a file.
Options can be adjusted in the code in the same way the infilename and outfilename are provided above. Refer to: -
https://github.com/scour-project/scour/wiki/Documentation
or just do >scour --help in the command prompt.

How can i use the variable in the path for source directory and destination using groovy

git1 repository have a folder named sports which further have subfolders and files inside it.I want to copy the file given by the output of git diff given by command2.
def command2 = "git diff --stat #{12.hours.ago}"
Process process = command2.execute(null, new File('C:/git1'))
def b=process.text
println b
Output of Groovy Console is Sports/Cricket/Players/Virat.txt.
Now i want to use this file path in sourceDir as shown below.
def sourceDir = "C:/git1/$b"
def destinationDir = "D:/git1/$b"
(new AntBuilder()).copy(file: sourceDir, tofile: destinationDir)
This is giving error as Warning: Could not find file C:\git1\Sports\Cricket\Players\Virat.txt
to copy.
As you can see in your comment with the byte array, the last character of the string is a newline character (10). This of course makes the path invalid but is not easily recognizable in the error message. Add a .trim() after the .text and it should work with both, AntBuilder and Files and on both, Windows and Linux. If you would have done it on Windows there would be a 13 additionally before the 10.
Another maybe even better approach would be not to use git diff as you do, as it is a porcelain command. Porcelain command as their name suggests are not stable and their arguments and output can change any time. They are meant for human usage, not for scripts. Scripts should use plumbing commands which are much more stable in input and output. With the respective plumbing command you might not have had your problem.

What does it mean to invoke `make -f` with a target that appears to be setting a variable? (And why isn't it working for me?)

Summary
I am trying to understand a complicated chain of Makefiles, in order to get a build to succeed. I narrowed down my problem to this bit in our build script:
INF_RL=`make -f $BUILD_ROOT/Makefile BUILD_ROOT_MAKEFILE= show__BUILD_INF_RL`
$INF_RL/$BUILD_UTILS_RELDIR/BuildAll.sh
$INF_RL is being set to an empty string (or not being set). If I replace the first line with
INF_RL=/foo_rel_linx86/infrastructure_release/v8.0.14
in order to hardcode what I know $INF_RL is supposed to be, then the build goes smoothly. But I want to know how to fix this the proper way.
What I've Tried / Thought
My first thought was that make -f is failing. So I tried it in my shell:
% make -f $BUILD_ROOT/Makefile BUILD_ROOT_MAKEFILE= show__BUILD_INF_RL
% setenv | grep BUILD_ROOT
BUILD_ROOT=/userhome/andrew.cheong/TPS
Indeed, it returned an empty string. But what conclusion could I draw from this? I wasn't sure if the shell was the same thing as the environment / scope in which Make was chaining together its Makefiles. I abandoned this investigation.
Next, I looked into show__BUILD_INF_RL, which seemed to be defined in $BUILD_ROOT/Makefile:
BUILD_ROOT_MAKEFILE = 1
MAKE_DIRS = src
CASE_KITS = tpsIn tpsOut
REQUIRED_VERSIONS = "case.v$(INF_VS)"
all:
## These next 3 rules allows any variable set in this makefile (and therefore
## the included makefile.include to have it's value echoed from the command
## "make show_<variableName>"
## NOTE: the "disp" target is vital as it allows the show_% implicit rule to be
## recognised as such - implicit rules *must* have a target.
show_% := DISPLAY_MACRO = $(#:show_%=%)
show_% : disp
# echo $($(DISPLAY_MACRO))
disp:
include $(BUILD_ROOT)/makefile.include
Here, I faced more questions:
What is BUILD_ROOT_MAKEFILE for? Why is it set to 1, then seemingly something else in the make -f command?
In the make -f command, is BUILD_ROOT_MAKEFILE= its own argument? If so, what kind of target or rule is that? Otherwise, why is it being set to the macro?
In $BUILD_ROOT, there is another file, makefile.LINUX_X86.include:
BUILD_INF_RL = /foo_rel_linx86/infrastructure_release/v$(INF_VS)
$(warning $(BUILD_INF_RL))
BUILD_UTILS = $(BUILD_INF_RL)/build-utils_LINUX_X86
Though a completely ignorant guess, I think BUILD_INF_RL is being set here, and intended to be extracted into the build script's variable INF_RL when the macro show__BUILD_INF_RL is invoked. I added the middle line to see if it was indeed being set, and indeed, I get this output when running the build script:
/userhome/andrew.cheong/TPS/makefile.LINUX_X86.include:3: /foo_rel_linx86/infrastructure_release/v8.0.14
i.e. Looks like what I've hardcoded way above! But why doesn't it make it into INF_RL? There is yet another file, makefile.include, also in $BUILD_ROOT:
#
# INCLUDE THIS FILE AS THE LAST LINE IN THE LOCAL MAKEFILE
#
# makefile.include - use this file to define global build settings
# e.g. infrastructure version and location, or third-party
#
# supported macros in addition to build-utils-makefile.include
#
# BUILD_INF_RL : optional, specification of infrastructure release location
# defaults to vdev_build area
#
include $(BUILD_ROOT)/../../makefile.include.$(BUILD_ARCH).Versions
#include $(BUILD_UTILS)/makefile.archdef.include
include $(BUILD_ROOT)/makefile.$(BUILD_ARCH).include
$(warning $(BUILD_INF_RL))
_BUILD_INF_RL = $(BUILD_INF_RL)
# place the results at the root of the infdemo tree
BUILD_DEST = $(BUILD_ROOT)
INCLUDE_DIRS += $(BUILD_INF_RL)/core/$(BUILD_TARGET)/include
LINK_DIRS += $(BUILD_INF_RL)/core/$(BUILD_TARGET)/lib
# libraries required for a typical fidessa app, including OA and DB access
FIDEVMAPP_LIBS = FidApp FidInf FidCore Fidevm
include $(BUILD_UTILS)/makefile.include
That $(warning ...) is again mine, and when running the build script, I get:
/userhome/andrew.cheong/TPS/makefile.include:18: /foo_rel_linx86/infrastructure_release/v8.0.14
The Question
The fact that both $(warning ...)s show up when I run the build script that's calling the make -f ... show__BUILD_INF_RL, tells me that those Makefiles are being included. Then what is causing the macro to fail and return an empty string instead of the correct INF_RL path?
Historical Notes
These build scripts were written at a time when we were only compiling for Solaris. (The scripts were based on templates written by an infrastructure team that loosely accounted for both Solaris and Linux, but we never ran the Linux branch, as it was unnecessary.) We are now fully migrating to Linux, and hitting this issue. The reason I'm skeptical of it being a Linux versus Solaris issue is that we have at least four other products that use a similar Makefile chain and have been migrated with no issues. Not sure why this one in particular is behaving different.
Your question got very long and complex so I didn't read it all... for SO it's often better if you just ask a specific targeted question that you want to know the answer to, with a simple repro case.
I can't say why different makefiles behave differently, but this line:
show_% := DISPLAY_MACRO = $(#:show_%=%)
seems really wrong to me. This is (a) setting the variable show_%, which don't actually use anywhere, (b) to the simply expanded string DISPLAY_MACRO = because at this point in the makefile the variable $# is not set to any value.
Maybe you wanted this line to be this instead:
show_% : DISPLAY_MACRO = $(#:show_%=%)
(note : not :=) so that it's a pattern-specific variable assignment, not a simple variable assignment?

Resources