Optimal way to filter DataFrame by many arguments, without usage of multiple ifs and for loops
Comparisons to None should always be done with is or is not. This is because None is a singleton object and this will give you the right behavior even if you override some objects __eq__ method or similar. This is mentioned in Python's official style-guide, PEP8. It also recommends no spaces around = for keyword arguments and using lower_case for variables and functions.

As to your actual question, what you really want to do is apply some function (fitting and plotting) to either the whole dataset or grouped by one or more categorical values.
The latter is what pandas.DataFrame.groupby is for.

kmf = KaplanMeierFitter()

def plot_fit(df, fig, name='Survival function'):
    results = kmf.fit(df['TIME'], df['EVENT'])
    trace_df = results.survival_function_.copy()  # Is this copy really needed?
    fig.add_trace(go.Scatter(
        x=trace_df.index,
        y=trace_df.iloc[:,0],
        mode='lines',
        name=name
    ))

def do_fit(df, by=None):
    fig = go.Figure()
    if by is None:
        plot_fit(df, fig)
    else:
        for group, df2 in df.groupby(by):
            name = ...  # Somehow determine label from group
            plot_fit(df2, fig, name)
    fig.update_layout(showlegend=True)
    return fig


This function can be called either as do_fit(df), do_fit(df, "PROTOCOL") or do_fit(df, ["PROTOCOL", "MUTATION"]). The group variable is a tuple telling you which protocol and/or mutation it is, extracting a correct label from this is left as an exercise. I also omitted getting the dataframe with your getter function and other preparations. These should probably be done in the function calling this function.

It could be argued that this should be divided up even further and the fitting should be separated from the plotting.