Skip to content

Commit 0384020

Browse files
committedMar 29, 2021
feat: basic pip fix -r support
1 parent f94c558 commit 0384020

File tree

9 files changed

+258
-78
lines changed

9 files changed

+258
-78
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { CustomError, ERROR_CODES } from './custom-error';
2+
3+
export class MissingFileNameError extends CustomError {
4+
public constructor() {
5+
super(
6+
'Filename is missing from test result. Please contact support@snyk.io.',
7+
ERROR_CODES.MissingFileName,
8+
);
9+
}
10+
}

‎packages/snyk-fix/src/plugins/python/handlers/pip-requirements/index.ts

+96-21
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as debugLib from 'debug';
22
import * as pathLib from 'path';
33

44
import {
5+
DependencyPins,
56
EntityToFix,
67
FixChangesSummary,
78
FixOptions,
@@ -18,6 +19,11 @@ import {
1819
extractProvenance,
1920
PythonProvenance,
2021
} from './extract-version-provenance';
22+
import {
23+
ParsedRequirements,
24+
parseRequirementsFile,
25+
Requirement,
26+
} from './update-dependencies/requirements-file-parser';
2127

2228
const debug = debugLib('snyk-fix:python:requirements.txt');
2329

@@ -37,17 +43,18 @@ export async function pipRequirementsTxt(
3743

3844
for (const entity of fixable) {
3945
try {
40-
const { remediation, targetFile, workspace } = getRequiredData(entity);
41-
const { dir, base } = pathLib.parse(targetFile);
42-
const provenance = await extractProvenance(workspace, dir, base);
43-
const changes = await fixIndividualRequirementsTxt(
44-
workspace,
45-
dir,
46-
base,
47-
remediation,
48-
provenance,
46+
const { changes } = await applyAllFixes(
47+
entity,
48+
// dir,
49+
// base,
50+
// remediation,
51+
// provenance,
4952
options,
5053
);
54+
if (!changes.length) {
55+
debug('Manifest has not changed!');
56+
throw new NoFixesCouldBeAppliedError();
57+
}
5158
handlerResult.succeeded.push({ original: entity, changes });
5259
} catch (e) {
5360
handlerResult.failed.push({ original: entity, error: e });
@@ -82,27 +89,95 @@ export function getRequiredData(
8289
export async function fixIndividualRequirementsTxt(
8390
workspace: Workspace,
8491
dir: string,
92+
entryFileName: string,
8593
fileName: string,
8694
remediation: RemediationChanges,
87-
provenance: PythonProvenance,
95+
parsedRequirements: ParsedRequirements,
8896
options: FixOptions,
89-
): Promise<FixChangesSummary[]> {
90-
// TODO: allow handlers per fix type (later also strategies or combine with strategies)
91-
const { updatedManifest, changes } = updateDependencies(
92-
provenance[fileName],
97+
directUpgradesOnly: boolean,
98+
): Promise<{ changes: FixChangesSummary[]; appliedRemediation: string[] }> {
99+
const fullFilePath = pathLib.join(dir, fileName);
100+
const { updatedManifest, changes, appliedRemediation } = updateDependencies(
101+
parsedRequirements,
93102
remediation.pin,
103+
directUpgradesOnly,
104+
pathLib.join(dir, entryFileName) !== fullFilePath ? fileName : undefined,
94105
);
95-
96-
if (!changes.length) {
97-
debug('Manifest has not changed!');
98-
throw new NoFixesCouldBeAppliedError();
99-
}
100-
if (!options.dryRun) {
106+
if (!options.dryRun && changes.length > 0) {
101107
debug('Writing changes to file');
102108
await workspace.writeFile(pathLib.join(dir, fileName), updatedManifest);
103109
} else {
104110
debug('Skipping writing changes to file in --dry-run mode');
105111
}
106112

107-
return changes;
113+
return { changes, appliedRemediation };
114+
}
115+
116+
export async function applyAllFixes(
117+
entity: EntityToFix,
118+
options: FixOptions,
119+
): Promise<{ changes: FixChangesSummary[] }> {
120+
const { remediation, targetFile: entryFileName, workspace } = getRequiredData(
121+
entity,
122+
);
123+
const { dir, base } = pathLib.parse(entryFileName);
124+
const provenance = await extractProvenance(workspace, dir, base);
125+
const upgradeChanges: FixChangesSummary[] = [];
126+
const appliedUpgradeRemediation: string[] = [];
127+
for (const fileName of Object.keys(provenance)) {
128+
const skipApplyingPins = true;
129+
const { changes, appliedRemediation } = await fixIndividualRequirementsTxt(
130+
workspace,
131+
dir,
132+
base,
133+
fileName,
134+
remediation,
135+
provenance[fileName],
136+
options,
137+
skipApplyingPins,
138+
);
139+
appliedUpgradeRemediation.push(...appliedRemediation);
140+
// what if we saw the file before and already fixed it?
141+
upgradeChanges.push(...changes);
142+
}
143+
// now do left overs as pins + add tests
144+
const requirementsTxt = await workspace.readFile(entryFileName);
145+
146+
const toPin: RemediationChanges = filterOutAppliedUpgrades(
147+
remediation,
148+
appliedUpgradeRemediation,
149+
);
150+
const directUpgradesOnly = false;
151+
const { changes: pinnedChanges } = await fixIndividualRequirementsTxt(
152+
workspace,
153+
dir,
154+
base,
155+
base,
156+
toPin,
157+
parseRequirementsFile(requirementsTxt),
158+
options,
159+
directUpgradesOnly,
160+
);
161+
162+
return { changes: [...upgradeChanges, ...pinnedChanges] };
163+
}
164+
165+
function filterOutAppliedUpgrades(
166+
remediation: RemediationChanges,
167+
appliedRemediation: string[],
168+
): RemediationChanges {
169+
const pinRemediation: RemediationChanges = {
170+
...remediation,
171+
pin: {}, // delete the pin remediation so we can add only not applied
172+
};
173+
const pins = remediation.pin;
174+
const lowerCasedAppliedRemediation = appliedRemediation.map((i) =>
175+
i.toLowerCase(),
176+
);
177+
for (const pkgAtVersion of Object.keys(pins)) {
178+
if (!lowerCasedAppliedRemediation.includes(pkgAtVersion.toLowerCase())) {
179+
pinRemediation.pin[pkgAtVersion] = pins[pkgAtVersion];
180+
}
181+
}
182+
return pinRemediation;
108183
}

‎packages/snyk-fix/src/plugins/python/handlers/pip-requirements/update-dependencies/generate-pins.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { Requirement } from './requirements-file-parser';
66
export function generatePins(
77
requirements: Requirement[],
88
updates: DependencyPins,
9-
): { pinnedRequirements: string[]; changes: FixChangesSummary[] } {
9+
): {
10+
pinnedRequirements: string[];
11+
changes: FixChangesSummary[];
12+
appliedRemediation: string[];
13+
} {
1014
// Lowercase the upgrades object. This might be overly defensive, given that
1115
// we control this input internally, but its a low cost guard rail. Outputs a
1216
// mapping of upgrade to -> from, instead of the nested upgradeTo object.
@@ -20,8 +24,10 @@ export function generatePins(
2024
return {
2125
pinnedRequirements: [],
2226
changes: [],
27+
appliedRemediation: [],
2328
};
2429
}
30+
const appliedRemediation: string[] = [];
2531
const changes: FixChangesSummary[] = [];
2632
const pinnedRequirements = Object.keys(lowerCasedPins)
2733
.map((pkgNameAtVersion) => {
@@ -32,12 +38,14 @@ export function generatePins(
3238
success: true,
3339
userMessage: `Pinned ${pkgName} from ${version} to ${newVersion}`,
3440
});
41+
appliedRemediation.push(pkgNameAtVersion);
3542
return `${newRequirement} # not directly required, pinned by Snyk to avoid a vulnerability`;
3643
})
3744
.filter(isDefined);
3845

3946
return {
4047
pinnedRequirements,
4148
changes,
49+
appliedRemediation,
4250
};
4351
}

‎packages/snyk-fix/src/plugins/python/handlers/pip-requirements/update-dependencies/generate-upgrades.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { UpgradedRequirements } from './types';
66
export function generateUpgrades(
77
requirements: Requirement[],
88
updates: DependencyPins,
9-
): { updatedRequirements: UpgradedRequirements; changes: FixChangesSummary[] } {
9+
referenceFileInChanges?: string,
10+
): {
11+
updatedRequirements: UpgradedRequirements;
12+
changes: FixChangesSummary[];
13+
appliedRemediation: string[];
14+
} {
1015
// Lowercase the upgrades object. This might be overly defensive, given that
1116
// we control this input internally, but its a low cost guard rail. Outputs a
1217
// mapping of upgrade to -> from, instead of the nested upgradeTo object.
@@ -19,9 +24,11 @@ export function generateUpgrades(
1924
return {
2025
updatedRequirements: {},
2126
changes: [],
27+
appliedRemediation: [],
2228
};
2329
}
2430

31+
const appliedRemediation: string[] = [];
2532
const changes: FixChangesSummary[] = [];
2633
const updatedRequirements = {};
2734
requirements.map(
@@ -58,16 +65,22 @@ export function generateUpgrades(
5865
const updatedRequirement = `${originalName}${versionComparator}${newVersion}`;
5966
changes.push({
6067
success: true,
61-
userMessage: `Upgraded ${originalName} from ${version} to ${newVersion}`,
68+
userMessage: `Upgraded ${originalName} from ${version} to ${newVersion}${
69+
referenceFileInChanges
70+
? ` (upgraded in ${referenceFileInChanges})`
71+
: ''
72+
}`,
6273
});
6374
updatedRequirements[originalText] = `${updatedRequirement}${
6475
extras ? extras : ''
6576
}`;
77+
appliedRemediation.push(upgrade);
6678
},
6779
);
6880

6981
return {
7082
updatedRequirements,
7183
changes,
84+
appliedRemediation,
7285
};
7386
}

‎packages/snyk-fix/src/plugins/python/handlers/pip-requirements/update-dependencies/index.ts

+18-9
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ export function updateDependencies(
2020
parsedRequirementsData: ParsedRequirements,
2121
updates: DependencyPins,
2222
directUpgradesOnly = false,
23-
): { updatedManifest: string; changes: FixChangesSummary[] } {
23+
referenceFileInChanges?: string,
24+
): {
25+
updatedManifest: string;
26+
changes: FixChangesSummary[];
27+
appliedRemediation: string[];
28+
} {
2429
const {
2530
requirements,
2631
endsWithNewLine: shouldEndWithNewLine,
@@ -33,19 +38,22 @@ export function updateDependencies(
3338
}
3439
debug('Finished parsing manifest');
3540

36-
const { updatedRequirements, changes: upgradedChanges } = generateUpgrades(
37-
requirements,
38-
updates,
39-
);
41+
const {
42+
updatedRequirements,
43+
changes: upgradedChanges,
44+
appliedRemediation,
45+
} = generateUpgrades(requirements, updates, referenceFileInChanges);
4046
debug('Finished generating upgrades to apply');
4147

4248
let pinnedRequirements: string[] = [];
4349
let pinChanges: FixChangesSummary[] = [];
50+
let appliedPinsRemediation: string[] = [];
4451
if (!directUpgradesOnly) {
45-
({ pinnedRequirements, changes: pinChanges } = generatePins(
46-
requirements,
47-
updates,
48-
));
52+
({
53+
pinnedRequirements,
54+
changes: pinChanges,
55+
appliedRemediation: appliedPinsRemediation,
56+
} = generatePins(requirements, updates));
4957
debug('Finished generating pins to apply');
5058
}
5159

@@ -64,5 +72,6 @@ export function updateDependencies(
6472
return {
6573
updatedManifest,
6674
changes: [...pinChanges, ...upgradedChanges],
75+
appliedRemediation: [...appliedPinsRemediation, ...appliedRemediation],
6776
};
6877
}

‎packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/__snapshots__/update-dependencies.spec.ts.snap

+13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`fix *req*.txt / *.txt Python projects fixes multiple files that are included via -r 1`] = `
4+
Array [
5+
Object {
6+
"success": true,
7+
"userMessage": "Upgraded Django from 1.6.1 to 2.0.1",
8+
},
9+
Object {
10+
"success": true,
11+
"userMessage": "Upgraded Jinja2 from 2.7.2 to 2.7.3 (upgraded in base2.txt)",
12+
},
13+
]
14+
`;
15+
316
exports[`fix *req*.txt / *.txt Python projects retains python markers 1`] = `
417
"amqp==2.4.2
518
apscheduler==3.6.0

0 commit comments

Comments
 (0)
Please sign in to comment.